summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE.md3
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md3
-rw-r--r--.gitignore78
-rw-r--r--.gitlab-ci.yml354
-rw-r--r--.rubocop.yml295
-rw-r--r--.vagrant_enabled (renamed from app/views/projects/notes/_commit_discussion.html.haml)0
-rw-r--r--CHANGELOG480
-rw-r--r--CONTRIBUTING.md14
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile84
-rw-r--r--Gemfile.lock461
-rw-r--r--README.md2
-rwxr-xr-xRakefile2
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin986 -> 695 bytes
-rw-r--r--app/assets/images/auth_buttons/bitbucket_64.pngbin2163 -> 2161 bytes
-rw-r--r--app/assets/images/auth_buttons/facebook_64.pngbin2970 -> 870 bytes
-rw-r--r--app/assets/images/auth_buttons/github_64.pngbin2625 -> 1151 bytes
-rw-r--r--app/assets/images/auth_buttons/gitlab_64.pngbin2849 -> 2070 bytes
-rw-r--r--app/assets/images/auth_buttons/google_64.pngbin5281 -> 4366 bytes
-rw-r--r--app/assets/images/auth_buttons/twitter_64.pngbin4835 -> 3110 bytes
-rw-r--r--app/assets/images/bg_fallback.pngbin167 -> 167 bytes
-rw-r--r--app/assets/images/ci/arch.jpgbin25222 -> 0 bytes
-rw-r--r--app/assets/images/ci/favicon.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/ci/loader.gifbin4405 -> 0 bytes
-rw-r--r--app/assets/images/ci/no_avatar.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/ci/rails.pngbin6646 -> 0 bytes
-rw-r--r--app/assets/images/ci/service_sample.pngbin76024 -> 0 bytes
-rw-r--r--app/assets/images/dark-scheme-preview.pngbin3996 -> 3992 bytes
-rw-r--r--app/assets/images/emoji.pngbin263533 -> 1025831 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin690504 -> 2492919 bytes
-rw-r--r--app/assets/images/gitlab_logo.pngbin5189 -> 3616 bytes
-rw-r--r--app/assets/images/gitorious-logo-black.pngbin809 -> 631 bytes
-rw-r--r--app/assets/images/gitorious-logo-blue.pngbin495 -> 201 bytes
-rw-r--r--app/assets/images/icon-link.pngbin1128 -> 729 bytes
-rw-r--r--app/assets/images/images.pngbin5849 -> 5806 bytes
-rw-r--r--app/assets/images/mailers/gitlab_header_logo.pngbin0 -> 7096 bytes
-rw-r--r--app/assets/images/mailers/gitlab_tanuki_2x.pngbin0 -> 2545 bytes
-rw-r--r--app/assets/images/monokai-scheme-preview.pngbin3711 -> 3708 bytes
-rw-r--r--app/assets/images/msapplication-tile.pngbin5798 -> 4328 bytes
-rw-r--r--app/assets/images/no_avatar.pngbin621 -> 621 bytes
-rw-r--r--app/assets/images/no_group_avatar.pngbin942 -> 939 bytes
-rw-r--r--app/assets/images/slider_handles.pngbin1377 -> 1341 bytes
-rw-r--r--app/assets/images/touch-icon-ipad-retina.pngbin8130 -> 5662 bytes
-rw-r--r--app/assets/images/touch-icon-ipad.pngbin3493 -> 2465 bytes
-rw-r--r--app/assets/images/touch-icon-iphone-retina.pngbin4997 -> 3460 bytes
-rw-r--r--app/assets/images/touch-icon-iphone.pngbin2766 -> 1949 bytes
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee92
-rw-r--r--app/assets/javascripts/activities.js.coffee5
-rw-r--r--app/assets/javascripts/api.js.coffee42
-rw-r--r--app/assets/javascripts/application.js.coffee163
-rw-r--r--app/assets/javascripts/awards_handler.coffee433
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.coffee23
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js.coffee5
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee35
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee5
-rw-r--r--app/assets/javascripts/blob/template_selector.js.coffee60
-rw-r--r--app/assets/javascripts/calendar.js.coffee34
-rw-r--r--app/assets/javascripts/ci/build.coffee94
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee33
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee4
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee57
-rw-r--r--app/assets/javascripts/flash.js.coffee14
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee129
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee216
-rw-r--r--app/assets/javascripts/gl_form.js.coffee3
-rw-r--r--app/assets/javascripts/graphs/application.js.coffee7
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js.coffee (renamed from app/assets/javascripts/stat_graph.js.coffee)0
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors.js.coffee)1
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors_graph.js.coffee)8
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors_util.js.coffee)0
-rw-r--r--app/assets/javascripts/importer_status.js.coffee11
-rw-r--r--app/assets/javascripts/issuable.js.coffee125
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee30
-rw-r--r--app/assets/javascripts/issue.js.coffee2
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee7
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js.coffee127
-rw-r--r--app/assets/javascripts/issues.js.coffee38
-rw-r--r--app/assets/javascripts/labels_select.js.coffee121
-rw-r--r--app/assets/javascripts/layout_nav.js.coffee24
-rw-r--r--app/assets/javascripts/lib/chart.js.coffee1
-rw-r--r--app/assets/javascripts/lib/d3.js.coffee1
-rw-r--r--app/assets/javascripts/lib/raphael.js.coffee3
-rw-r--r--app/assets/javascripts/lib/utils/animate.js.coffee (renamed from app/assets/javascripts/lib/animate.js.coffee)0
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.coffee65
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.coffee (renamed from app/assets/javascripts/lib/datetime_utility.js.coffee)9
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb2
-rw-r--r--app/assets/javascripts/lib/utils/jquery.timeago.js (renamed from app/assets/javascripts/lib/jquery.timeago.js)0
-rw-r--r--app/assets/javascripts/lib/utils/md5.js (renamed from app/assets/javascripts/lib/md5.js)0
-rw-r--r--app/assets/javascripts/lib/utils/notify.js.coffee (renamed from app/assets/javascripts/lib/notify.js.coffee)0
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js.coffee105
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js.coffee9
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.coffee (renamed from app/assets/javascripts/lib/url_utility.js.coffee)11
-rw-r--r--app/assets/javascripts/lib/utils/utf8_encode.js (renamed from app/assets/javascripts/lib/utf8_encode.js)0
-rw-r--r--app/assets/javascripts/logo.js.coffee6
-rw-r--r--app/assets/javascripts/merge_request.js.coffee2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee5
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee15
-rw-r--r--app/assets/javascripts/merged_buttons.js.coffee30
-rw-r--r--app/assets/javascripts/milestone.js.coffee63
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee19
-rw-r--r--app/assets/javascripts/namespace_select.js.coffee79
-rw-r--r--app/assets/javascripts/network/application.js.coffee17
-rw-r--r--app/assets/javascripts/network/branch-graph.js.coffee (renamed from app/assets/javascripts/branch-graph.js.coffee)0
-rw-r--r--app/assets/javascripts/network/network.js.coffee (renamed from app/assets/javascripts/network.js.coffee)0
-rw-r--r--app/assets/javascripts/notes.js.coffee100
-rw-r--r--app/assets/javascripts/notifications_dropdown.js.coffee25
-rw-r--r--app/assets/javascripts/notifications_form.js.coffee49
-rw-r--r--app/assets/javascripts/pager.js.coffee3
-rw-r--r--app/assets/javascripts/profile.js.coffee4
-rw-r--r--app/assets/javascripts/project.js.coffee57
-rw-r--r--app/assets/javascripts/project_new.js.coffee19
-rw-r--r--app/assets/javascripts/protected_branch_select.js.coffee40
-rw-r--r--app/assets/javascripts/protected_branches.js.coffee3
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee91
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee98
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee10
-rw-r--r--app/assets/javascripts/shortcuts_blob.coffee10
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee8
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee18
-rw-r--r--app/assets/javascripts/sidebar.js.coffee33
-rw-r--r--app/assets/javascripts/star.js.coffee2
-rw-r--r--app/assets/javascripts/subscription.js.coffee5
-rw-r--r--app/assets/javascripts/tree.js.coffee12
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/error.js.coffee13
-rw-r--r--app/assets/javascripts/u2f/register.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/util.js.coffee.erb15
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee12
-rw-r--r--app/assets/javascripts/users/application.js.coffee2
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee193
-rw-r--r--app/assets/javascripts/users_select.js.coffee22
-rw-r--r--app/assets/stylesheets/application.scss2
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss72
-rw-r--r--app/assets/stylesheets/framework/avatar.scss1
-rw-r--r--app/assets/stylesheets/framework/blank.scss44
-rw-r--r--app/assets/stylesheets/framework/blocks.scss36
-rw-r--r--app/assets/stylesheets/framework/buttons.scss69
-rw-r--r--app/assets/stylesheets/framework/calendar.scss72
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss75
-rw-r--r--app/assets/stylesheets/framework/files.scss33
-rw-r--r--app/assets/stylesheets/framework/flash.scss7
-rw-r--r--app/assets/stylesheets/framework/forms.scss9
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss70
-rw-r--r--app/assets/stylesheets/framework/header.scss113
-rw-r--r--app/assets/stylesheets/framework/jquery.scss43
-rw-r--r--app/assets/stylesheets/framework/lists.scss33
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss31
-rw-r--r--app/assets/stylesheets/framework/mixins.scss22
-rw-r--r--app/assets/stylesheets/framework/mobile.scss25
-rw-r--r--app/assets/stylesheets/framework/nav.scss215
-rw-r--r--app/assets/stylesheets/framework/panels.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss7
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss332
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss5
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss33
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/mailers/devise.scss138
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss182
-rw-r--r--app/assets/stylesheets/notify.scss16
-rw-r--r--app/assets/stylesheets/pages/admin.scss33
-rw-r--r--app/assets/stylesheets/pages/awards.scss46
-rw-r--r--app/assets/stylesheets/pages/builds.scss98
-rw-r--r--app/assets/stylesheets/pages/commit.scss26
-rw-r--r--app/assets/stylesheets/pages/commits.scss138
-rw-r--r--app/assets/stylesheets/pages/confirmation.scss10
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss15
-rw-r--r--app/assets/stylesheets/pages/editor.scss27
-rw-r--r--app/assets/stylesheets/pages/environments.scss5
-rw-r--r--app/assets/stylesheets/pages/events.scss7
-rw-r--r--app/assets/stylesheets/pages/groups.scss49
-rw-r--r--app/assets/stylesheets/pages/help.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss44
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss149
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss76
-rw-r--r--app/assets/stylesheets/pages/note_form.scss63
-rw-r--r--app/assets/stylesheets/pages/notes.scss99
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss24
-rw-r--r--app/assets/stylesheets/pages/profile.scss33
-rw-r--r--app/assets/stylesheets/pages/projects.scss452
-rw-r--r--app/assets/stylesheets/pages/search.scss8
-rw-r--r--app/assets/stylesheets/pages/settings.scss8
-rw-r--r--app/assets/stylesheets/pages/snippets.scss44
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss22
-rw-r--r--app/assets/stylesheets/pages/todos.scss18
-rw-r--r--app/assets/stylesheets/pages/tree.scss18
-rw-r--r--app/assets/stylesheets/pages/xterm.scss19
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb2
-rw-r--r--app/controllers/admin/appearances_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb1
-rw-r--r--app/controllers/admin/health_check_controller.rb5
-rw-r--r--app/controllers/admin/hooks_controller.rb1
-rw-r--r--app/controllers/admin/keys_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb8
-rw-r--r--app/controllers/admin/runner_projects_controller.rb9
-rw-r--r--app/controllers/admin/runners_controller.rb37
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/controllers/admin/system_info_controller.rb59
-rw-r--r--app/controllers/admin/users_controller.rb3
-rw-r--r--app/controllers/application_controller.rb49
-rw-r--r--app/controllers/autocomplete_controller.rb19
-rw-r--r--app/controllers/ci/projects_controller.rb2
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb59
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb44
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb31
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb2
-rw-r--r--app/controllers/confirmations_controller.rb1
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb29
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb33
-rw-r--r--app/controllers/groups/notification_settings_controller.rb16
-rw-r--r--app/controllers/groups_controller.rb23
-rw-r--r--app/controllers/health_check_controller.rb22
-rw-r--r--app/controllers/import/base_controller.rb1
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb25
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb49
-rw-r--r--app/controllers/import/gitorious_controller.rb1
-rw-r--r--app/controllers/import/google_code_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb1
-rw-r--r--app/controllers/jwt_controller.rb49
-rw-r--r--app/controllers/notification_settings_controller.rb50
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/notifications_controller.rb9
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb42
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb45
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/artifacts_controller.rb19
-rw-r--r--app/controllers/projects/avatars_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb9
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb16
-rw-r--r--app/controllers/projects/commit_controller.rb32
-rw-r--r--app/controllers/projects/compare_controller.rb21
-rw-r--r--app/controllers/projects/container_registry_controller.rb34
-rw-r--r--app/controllers/projects/environments_controller.rb49
-rw-r--r--app/controllers/projects/find_file_controller.rb52
-rw-r--r--app/controllers/projects/git_http_controller.rb200
-rw-r--r--app/controllers/projects/hooks_controller.rb9
-rw-r--r--app/controllers/projects/imports_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb22
-rw-r--r--app/controllers/projects/labels_controller.rb29
-rw-r--r--app/controllers/projects/merge_requests_controller.rb166
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/network_controller.rb1
-rw-r--r--app/controllers/projects/notes_controller.rb79
-rw-r--r--app/controllers/projects/notification_settings_controller.rb16
-rw-r--r--app/controllers/projects/pipelines_controller.rb59
-rw-r--r--app/controllers/projects/project_members_controller.rb39
-rw-r--r--app/controllers/projects/protected_branches_controller.rb28
-rw-r--r--app/controllers/projects/raw_controller.rb5
-rw-r--r--app/controllers/projects/repositories_controller.rb3
-rw-r--r--app/controllers/projects/runner_projects_controller.rb5
-rw-r--r--app/controllers/projects/runners_controller.rb11
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb6
-rw-r--r--app/controllers/projects/todos_controller.rb31
-rw-r--r--app/controllers/projects/variables_controller.rb30
-rw-r--r--app/controllers/projects/wikis_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb100
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb39
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/users_controller.rb24
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb18
-rw-r--r--app/finders/notes_finder.rb6
-rw-r--r--app/finders/pipelines_finder.rb38
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/finders/todos_finder.rb39
-rw-r--r--app/helpers/appearances_helper.rb4
-rw-r--r--app/helpers/application_helper.rb49
-rw-r--r--app/helpers/application_settings_helper.rb48
-rw-r--r--app/helpers/auth_helper.rb12
-rw-r--r--app/helpers/blob_helper.rb30
-rw-r--r--app/helpers/branches_helper.rb8
-rw-r--r--app/helpers/button_helper.rb36
-rw-r--r--app/helpers/ci_status_helper.rb35
-rw-r--r--app/helpers/commits_helper.rb43
-rw-r--r--app/helpers/diff_helper.rb50
-rw-r--r--app/helpers/dropdowns_helper.rb6
-rw-r--r--app/helpers/emails_helper.rb7
-rw-r--r--app/helpers/events_helper.rb92
-rw-r--r--app/helpers/gitlab_markdown_helper.rb19
-rw-r--r--app/helpers/gitlab_routing_helper.rb85
-rw-r--r--app/helpers/groups_helper.rb22
-rw-r--r--app/helpers/issuables_helper.rb30
-rw-r--r--app/helpers/issues_helper.rb33
-rw-r--r--app/helpers/javascript_helper.rb5
-rw-r--r--app/helpers/kerberos_spnego_helper.rb9
-rw-r--r--app/helpers/labels_helper.rb18
-rw-r--r--app/helpers/members_helper.rb45
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb36
-rw-r--r--app/helpers/notes_helper.rb98
-rw-r--r--app/helpers/notifications_helper.rb35
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb68
-rw-r--r--app/helpers/search_helper.rb3
-rw-r--r--app/helpers/selects_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb11
-rw-r--r--app/helpers/tab_helper.rb6
-rw-r--r--app/helpers/time_helper.rb8
-rw-r--r--app/helpers/todos_helper.rb33
-rw-r--r--app/helpers/workhorse_helper.rb31
-rw-r--r--app/mailers/devise_mailer.rb2
-rw-r--r--app/mailers/emails/groups.rb52
-rw-r--r--app/mailers/emails/members.rb86
-rw-r--r--app/mailers/emails/projects.rb72
-rw-r--r--app/mailers/notify.rb6
-rw-r--r--app/models/ability.rb121
-rw-r--r--app/models/application_setting.rb44
-rw-r--r--app/models/award_emoji.rb26
-rw-r--r--app/models/blob.rb2
-rw-r--r--app/models/ci/build.rb123
-rw-r--r--app/models/ci/pipeline.rb (renamed from app/models/ci/commit.rb)113
-rw-r--r--app/models/ci/runner.rb41
-rw-r--r--app/models/ci/trigger_request.rb4
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/models/commit.rb62
-rw-r--r--app/models/commit_range.rb6
-rw-r--r--app/models/commit_status.rb30
-rw-r--r--app/models/concerns/access_requestable.rb16
-rw-r--r--app/models/concerns/awardable.rb83
-rw-r--r--app/models/concerns/importable.rb6
-rw-r--r--app/models/concerns/issuable.rb156
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/note_on_diff.rb52
-rw-r--r--app/models/concerns/participable.rb102
-rw-r--r--app/models/concerns/referable.rb4
-rw-r--r--app/models/concerns/subscribable.rb6
-rw-r--r--app/models/deployment.rb35
-rw-r--r--app/models/diff_note.rb127
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/event.rb41
-rw-r--r--app/models/group.rb20
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/issue.rb45
-rw-r--r--app/models/jira_issue.rb2
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/label.rb28
-rw-r--r--app/models/legacy_diff_note.rb114
-rw-r--r--app/models/member.rb50
-rw-r--r--app/models/members/group_member.rb8
-rw-r--r--app/models/members/project_member.rb12
-rw-r--r--app/models/merge_request.rb266
-rw-r--r--app/models/merge_request_diff.rb167
-rw-r--r--app/models/milestone.rb73
-rw-r--r--app/models/namespace.rb97
-rw-r--r--app/models/network/graph.rb39
-rw-r--r--app/models/note.rb303
-rw-r--r--app/models/notification_setting.rb47
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb347
-rw-r--r--app/models/project_import_data.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb44
-rw-r--r--app/models/project_services/bugzilla_service.rb23
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb5
-rw-r--r--app/models/project_services/drone_ci_service.rb1
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb4
-rw-r--r--app/models/project_services/issue_tracker_service.rb19
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/models/project_services/redmine_service.rb1
-rw-r--r--app/models/project_services/slack_service/build_message.rb4
-rw-r--r--app/models/project_services/slack_service/issue_message.rb19
-rw-r--r--app/models/project_services/teamcity_service.rb37
-rw-r--r--app/models/project_snippet.rb3
-rw-r--r--app/models/project_team.rb52
-rw-r--r--app/models/project_wiki.rb22
-rw-r--r--app/models/protected_branch.rb47
-rw-r--r--app/models/repository.rb143
-rw-r--r--app/models/sent_notification.rb53
-rw-r--r--app/models/service.rb13
-rw-r--r--app/models/snippet.rb24
-rw-r--r--app/models/todo.rb29
-rw-r--r--app/models/u2f_registration.rb40
-rw-r--r--app/models/user.rb171
-rw-r--r--app/services/audit_event_service.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb89
-rw-r--r--app/services/ci/create_builds_service.rb57
-rw-r--r--app/services/ci/create_pipeline_service.rb48
-rw-r--r--app/services/ci/create_trigger_request_service.rb6
-rw-r--r--app/services/ci/image_for_build_service.rb6
-rw-r--r--app/services/ci/register_build_service.rb25
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/create_branch_service.rb14
-rw-r--r--app/services/create_commit_builds_service.rb60
-rw-r--r--app/services/create_deployment_service.rb18
-rw-r--r--app/services/create_release_service.rb5
-rw-r--r--app/services/create_snippet_service.rb10
-rw-r--r--app/services/create_tag_service.rb6
-rw-r--r--app/services/delete_branch_service.rb16
-rw-r--r--app/services/delete_tag_service.rb9
-rw-r--r--app/services/files/base_service.rb9
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/git_hooks_service.rb8
-rw-r--r--app/services/git_push_service.rb17
-rw-r--r--app/services/git_tag_push_service.rb3
-rw-r--r--app/services/issuable_base_service.rb34
-rw-r--r--app/services/issues/base_service.rb1
-rw-r--r--app/services/issues/bulk_update_service.rb7
-rw-r--r--app/services/issues/move_service.rb9
-rw-r--r--app/services/issues/update_service.rb10
-rw-r--r--app/services/members/destroy_service.rb21
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb17
-rw-r--r--app/services/merge_requests/base_service.rb26
-rw-r--r--app/services/merge_requests/build_service.rb4
-rw-r--r--app/services/merge_requests/create_service.rb7
-rw-r--r--app/services/merge_requests/merge_service.rb15
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb28
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/refresh_service.rb19
-rw-r--r--app/services/merge_requests/reopen_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/milestones/destroy_service.rb1
-rw-r--r--app/services/notes/create_service.rb16
-rw-r--r--app/services/notes/diff_position_update_service.rb30
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notification_service.rb176
-rw-r--r--app/services/oauth2/access_token_validation_service.rb1
-rw-r--r--app/services/projects/autocomplete_service.rb8
-rw-r--r--app/services/projects/create_service.rb53
-rw-r--r--app/services/projects/destroy_service.rb18
-rw-r--r--app/services/projects/download_service.rb1
-rw-r--r--app/services/projects/fork_service.rb14
-rw-r--r--app/services/projects/housekeeping_service.rb14
-rw-r--r--app/services/projects/import_export/export_service.rb60
-rw-r--r--app/services/projects/import_service.rb29
-rw-r--r--app/services/projects/transfer_service.rb9
-rw-r--r--app/services/projects/update_service.rb3
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/services/system_note_service.rb33
-rw-r--r--app/services/todo_service.rb79
-rw-r--r--app/services/update_release_service.rb5
-rw-r--r--app/services/update_snippet_service.rb1
-rw-r--r--app/services/wiki_pages/base_service.rb6
-rw-r--r--app/uploaders/lfs_object_uploader.rb2
-rw-r--r--app/validators/addressable_url_validator.rb45
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml6
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/appearances/preview.html.haml34
-rw-r--r--app/views/admin/appearances/show.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml50
-rw-r--r--app/views/admin/application_settings/show.html.haml1
-rw-r--r--app/views/admin/background_jobs/_head.html.haml18
-rw-r--r--app/views/admin/background_jobs/show.html.haml82
-rw-r--r--app/views/admin/builds/index.html.haml104
-rw-r--r--app/views/admin/dashboard/_head.html.haml26
-rw-r--r--app/views/admin/dashboard/index.html.haml300
-rw-r--r--app/views/admin/groups/_group.html.haml42
-rw-r--r--app/views/admin/groups/index.html.haml70
-rw-r--r--app/views/admin/groups/show.html.haml31
-rw-r--r--app/views/admin/health_check/show.html.haml52
-rw-r--r--app/views/admin/logs/show.html.haml52
-rw-r--r--app/views/admin/projects/index.html.haml166
-rw-r--r--app/views/admin/projects/show.html.haml53
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml127
-rw-r--r--app/views/admin/runners/show.html.haml32
-rw-r--r--app/views/admin/system_info/show.html.haml25
-rw-r--r--app/views/admin/users/_user.html.haml42
-rw-r--r--app/views/admin/users/groups.html.haml7
-rw-r--r--app/views/admin/users/index.html.haml175
-rw-r--r--app/views/admin/users/projects.html.haml3
-rw-r--r--app/views/award_emoji/_awards_block.html.haml15
-rw-r--r--app/views/ci/errors/show.haml2
-rw-r--r--app/views/ci/shared/_guide.html.haml13
-rw-r--r--app/views/ci/shared/_no_runners.html.haml7
-rw-r--r--app/views/dashboard/_groups_head.html.haml1
-rw-r--r--app/views/dashboard/_projects_head.html.haml3
-rw-r--r--app/views/dashboard/issues.atom.builder7
-rw-r--r--app/views/dashboard/projects/index.atom.builder4
-rw-r--r--app/views/dashboard/todos/_todo.html.haml14
-rw-r--r--app/views/devise/confirmations/almost_there.haml3
-rw-r--r--app/views/devise/mailer/confirmation_instructions.html.erb9
-rw-r--r--app/views/devise/mailer/confirmation_instructions.html.haml16
-rw-r--r--app/views/devise/mailer/confirmation_instructions.text.erb9
-rw-r--r--app/views/devise/mailer/password_change.html.haml10
-rw-r--r--app/views/devise/mailer/password_change.text.erb7
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.erb8
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.haml12
-rw-r--r--app/views/devise/mailer/reset_password_instructions.text.erb10
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml19
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb7
-rw-r--r--app/views/devise/sessions/new.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml21
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/emojis/index.html.haml6
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.atom.builder20
-rw-r--r--app/views/events/_event_issue.atom.haml2
-rw-r--r--app/views/events/_event_merge_request.atom.haml2
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/event/_common.html.haml10
-rw-r--r--app/views/events/event/_push.html.haml27
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/group_members/_group_member.html.haml57
-rw-r--r--app/views/groups/group_members/index.html.haml15
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/groups/issues.atom.builder15
-rw-r--r--app/views/groups/milestones/new.html.haml3
-rw-r--r--app/views/groups/show.atom.builder4
-rw-r--r--app/views/groups/show.html.haml10
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/import/base/create.js.haml5
-rw-r--r--app/views/import/github/new.html.haml43
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml25
-rw-r--r--app/views/issues/_issue.atom.builder32
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml6
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml2
-rw-r--r--app/views/layouts/_collapse_button.html.haml7
-rw-r--r--app/views/layouts/_head.html.haml5
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml22
-rw-r--r--app/views/layouts/_search.html.haml32
-rw-r--r--app/views/layouts/admin.html.haml2
-rw-r--r--app/views/layouts/application.html.haml4
-rw-r--r--app/views/layouts/ci/_info.html.haml2
-rw-r--r--app/views/layouts/ci/_page.html.haml28
-rw-r--r--app/views/layouts/ci/notify.html.haml19
-rw-r--r--app/views/layouts/devise.html.haml1
-rw-r--r--app/views/layouts/devise_empty.html.haml1
-rw-r--r--app/views/layouts/devise_mailer.html.haml34
-rw-r--r--app/views/layouts/errors.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml24
-rw-r--r--app/views/layouts/nav/_admin.html.haml134
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml31
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml43
-rw-r--r--app/views/layouts/nav/_group.html.haml72
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml36
-rw-r--r--app/views/layouts/nav/_profile.html.haml96
-rw-r--r--app/views/layouts/nav/_project.html.haml234
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml96
-rw-r--r--app/views/layouts/notify.html.haml1
-rw-r--r--app/views/layouts/project.html.haml6
-rw-r--r--app/views/layouts/project_settings.html.haml3
-rw-r--r--app/views/notify/_note_message.html.haml2
-rw-r--r--app/views/notify/build_fail_email.html.haml4
-rw-r--r--app/views/notify/build_fail_email.text.erb6
-rw-r--r--app/views/notify/build_success_email.html.haml4
-rw-r--r--app/views/notify/build_success_email.text.erb6
-rw-r--r--app/views/notify/group_access_granted_email.html.haml4
-rw-r--r--app/views/notify/group_access_granted_email.text.erb4
-rw-r--r--app/views/notify/group_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/group_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/group_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/group_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/group_member_invited_email.html.haml14
-rw-r--r--app/views/notify/group_member_invited_email.text.erb4
-rw-r--r--app/views/notify/member_access_denied_email.html.haml4
-rw-r--r--app/views/notify/member_access_denied_email.text.erb3
-rw-r--r--app/views/notify/member_access_granted_email.html.haml3
-rw-r--r--app/views/notify/member_access_granted_email.text.erb3
-rw-r--r--app/views/notify/member_access_requested_email.html.haml3
-rw-r--r--app/views/notify/member_access_requested_email.text.erb3
-rw-r--r--app/views/notify/member_invite_accepted_email.html.haml5
-rw-r--r--app/views/notify/member_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/member_invite_declined_email.html.haml4
-rw-r--r--app/views/notify/member_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/member_invited_email.html.haml13
-rw-r--r--app/views/notify/member_invited_email.text.erb4
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/note_merge_request_email.html.haml4
-rw-r--r--app/views/notify/project_access_granted_email.html.haml5
-rw-r--r--app/views/notify/project_access_granted_email.text.erb4
-rw-r--r--app/views/notify/project_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/project_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/project_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/project_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/project_member_invited_email.html.haml13
-rw-r--r--app/views/notify/project_member_invited_email.text.erb4
-rw-r--r--app/views/notify/project_was_exported_email.html.haml8
-rw-r--r--app/views/notify/project_was_exported_email.text.erb6
-rw-r--r--app/views/notify/project_was_not_exported_email.html.haml9
-rw-r--r--app/views/notify/project_was_not_exported_email.text.haml6
-rw-r--r--app/views/notify/repository_push_email.html.haml58
-rw-r--r--app/views/notify/repository_push_email.text.haml38
-rw-r--r--app/views/profiles/accounts/show.html.haml39
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml5
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml5
-rw-r--r--app/views/profiles/notifications/show.html.haml35
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml105
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml39
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml69
-rw-r--r--app/views/projects/_builds_settings.html.haml119
-rw-r--r--app/views/projects/_github_import_modal.html.haml13
-rw-r--r--app/views/projects/_home_panel.html.haml63
-rw-r--r--app/views/projects/_last_commit.html.haml19
-rw-r--r--app/views/projects/_last_push.html.haml24
-rw-r--r--app/views/projects/_md_preview.html.haml21
-rw-r--r--app/views/projects/_merge_request_settings.html.haml11
-rw-r--r--app/views/projects/_zen.html.haml2
-rw-r--r--app/views/projects/activity.html.haml1
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/badges/index.html.haml3
-rw-r--r--app/views/projects/blame/show.html.haml1
-rw-r--r--app/views/projects/blob/_editor.html.haml9
-rw-r--r--app/views/projects/blob/_header_title.html.haml1
-rw-r--r--app/views/projects/blob/_text.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml3
-rw-r--r--app/views/projects/blob/new.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml18
-rw-r--r--app/views/projects/branches/_branch.html.haml8
-rw-r--r--app/views/projects/branches/destroy.js.haml1
-rw-r--r--app/views/projects/branches/index.html.haml61
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/builds/_header.html.haml16
-rw-r--r--app/views/projects/builds/_header_title.html.haml1
-rw-r--r--app/views/projects/builds/_sidebar.html.haml107
-rw-r--r--app/views/projects/builds/_user.html.haml4
-rw-r--r--app/views/projects/builds/index.html.haml126
-rw-r--r--app/views/projects/builds/show.html.haml191
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/buttons/_notifications.html.haml11
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml71
-rw-r--r--app/views/projects/commit/_builds.html.haml4
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/commit/_ci_commit.html.haml71
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml15
-rw-r--r--app/views/projects/commit/_commit_box.html.haml78
-rw-r--r--app/views/projects/commit/_pipeline.html.haml52
-rw-r--r--app/views/projects/commit/builds.html.haml4
-rw-r--r--app/views/projects/commit/show.html.haml5
-rw-r--r--app/views/projects/commits/_commit.atom.builder14
-rw-r--r--app/views/projects/commits/_commit.html.haml45
-rw-r--r--app/views/projects/commits/_commits.html.haml19
-rw-r--r--app/views/projects/commits/_head.html.haml46
-rw-r--r--app/views/projects/commits/_header_title.html.haml1
-rw-r--r--app/views/projects/commits/show.atom.builder15
-rw-r--r--app/views/projects/commits/show.html.haml59
-rw-r--r--app/views/projects/compare/index.html.haml27
-rw-r--r--app/views/projects/compare/show.html.haml37
-rw-r--r--app/views/projects/container_registry/_tag.html.haml29
-rw-r--r--app/views/projects/container_registry/index.html.haml39
-rw-r--r--app/views/projects/deploy_keys/index.html.haml4
-rw-r--r--app/views/projects/deployments/_commit.html.haml12
-rw-r--r--app/views/projects/deployments/_deployment.html.haml23
-rw-r--r--app/views/projects/diffs/_diffs.html.haml7
-rw-r--r--app/views/projects/diffs/_file.html.haml53
-rw-r--r--app/views/projects/diffs/_file_header.html.haml25
-rw-r--r--app/views/projects/diffs/_image.html.haml11
-rw-r--r--app/views/projects/diffs/_line.html.haml16
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml30
-rw-r--r--app/views/projects/diffs/_text_file.html.haml20
-rw-r--r--app/views/projects/edit.html.haml479
-rw-r--r--app/views/projects/empty.html.haml10
-rw-r--r--app/views/projects/environments/_environment.html.haml17
-rw-r--r--app/views/projects/environments/_form.html.haml7
-rw-r--r--app/views/projects/environments/_header_title.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml31
-rw-r--r--app/views/projects/environments/new.html.haml12
-rw-r--r--app/views/projects/environments/show.html.haml37
-rw-r--r--app/views/projects/find_file/show.html.haml1
-rw-r--r--app/views/projects/forks/index.html.haml6
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml15
-rw-r--r--app/views/projects/graphs/_head.html.haml27
-rw-r--r--app/views/projects/graphs/_header_title.html.haml1
-rw-r--r--app/views/projects/graphs/ci.html.haml26
-rw-r--r--app/views/projects/graphs/ci/_overall.haml2
-rw-r--r--app/views/projects/graphs/commits.html.haml91
-rw-r--r--app/views/projects/graphs/languages.html.haml37
-rw-r--r--app/views/projects/graphs/show.html.haml49
-rw-r--r--app/views/projects/hooks/_project_hook.html.haml2
-rw-r--r--app/views/projects/hooks/index.html.haml85
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_head.html.haml25
-rw-r--r--app/views/projects/issues/_header_title.html.haml1
-rw-r--r--app/views/projects/issues/_issue.html.haml6
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml9
-rw-r--r--app/views/projects/issues/_new_branch.html.haml12
-rw-r--r--app/views/projects/issues/_related_branches.html.haml6
-rw-r--r--app/views/projects/issues/edit.html.haml1
-rw-r--r--app/views/projects/issues/index.atom.builder6
-rw-r--r--app/views/projects/issues/index.html.haml53
-rw-r--r--app/views/projects/issues/new.html.haml1
-rw-r--r--app/views/projects/issues/show.html.haml17
-rw-r--r--app/views/projects/labels/_header_title.html.haml1
-rw-r--r--app/views/projects/labels/_label.html.haml62
-rw-r--r--app/views/projects/labels/edit.html.haml1
-rw-r--r--app/views/projects/labels/index.html.haml53
-rw-r--r--app/views/projects/labels/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_head.html.haml5
-rw-r--r--app/views/projects/merge_requests/_header_title.html.haml1
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml1
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml4
-rw-r--r--app/views/projects/merge_requests/_show.html.haml21
-rw-r--r--app/views/projects/merge_requests/dropdowns/_branch.html.haml2
-rw-r--r--app/views/projects/merge_requests/edit.html.haml1
-rw-r--r--app/views/projects/merge_requests/index.html.haml28
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml1
-rw-r--r--app/views/projects/merge_requests/merge.js.haml3
-rw-r--r--app/views/projects/merge_requests/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml6
-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.haml7
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml16
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml59
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml37
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/open/_not_allowed.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml11
-rw-r--r--app/views/projects/milestones/_header_title.html.haml1
-rw-r--r--app/views/projects/milestones/edit.html.haml1
-rw-r--r--app/views/projects/milestones/index.html.haml32
-rw-r--r--app/views/projects/milestones/new.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml6
-rw-r--r--app/views/projects/network/_head.html.haml13
-rw-r--r--app/views/projects/network/show.html.haml40
-rw-r--r--app/views/projects/new.html.haml229
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply.html.haml17
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml22
-rw-r--r--app/views/projects/notes/_discussion.html.haml47
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml4
-rw-r--r--app/views/projects/notes/_hints.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml28
-rw-r--r--app/views/projects/notes/_notes.html.haml9
-rw-r--r--app/views/projects/notes/discussions/_active.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml25
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml28
-rw-r--r--app/views/projects/notes/discussions/_diff_with_notes.html.haml17
-rw-r--r--app/views/projects/notes/discussions/_notes.html.haml6
-rw-r--r--app/views/projects/notes/discussions/_outdated.html.haml14
-rw-r--r--app/views/projects/pipelines/_head.html.haml19
-rw-r--r--app/views/projects/pipelines/_info.html.haml37
-rw-r--r--app/views/projects/pipelines/index.html.haml58
-rw-r--r--app/views/projects/pipelines/new.html.haml21
-rw-r--r--app/views/projects/pipelines/show.html.haml8
-rw-r--r--app/views/projects/project_members/_group_members.html.haml17
-rw-r--r--app/views/projects/project_members/_header_title.html.haml1
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_project_member.html.haml55
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml9
-rw-r--r--app/views/projects/project_members/_team.html.haml6
-rw-r--r--app/views/projects/project_members/import.html.haml1
-rw-r--r--app/views/projects/project_members/index.html.haml5
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml35
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml17
-rw-r--r--app/views/projects/protected_branches/_matching_branch.html.haml9
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml21
-rw-r--r--app/views/projects/protected_branches/index.html.haml24
-rw-r--r--app/views/projects/protected_branches/show.html.haml25
-rw-r--r--app/views/projects/releases/edit.html.haml1
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml38
-rw-r--r--app/views/projects/runners/_runner.html.haml8
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml12
-rw-r--r--app/views/projects/runners/edit.html.haml27
-rw-r--r--app/views/projects/runners/show.html.haml54
-rw-r--r--app/views/projects/services/_form.html.haml32
-rw-r--r--app/views/projects/services/index.html.haml52
-rw-r--r--app/views/projects/show.atom.builder4
-rw-r--r--app/views/projects/show.html.haml33
-rw-r--r--app/views/projects/snippets/_actions.html.haml38
-rw-r--r--app/views/projects/snippets/_header_title.html.haml1
-rw-r--r--app/views/projects/snippets/edit.html.haml1
-rw-r--r--app/views/projects/snippets/index.html.haml1
-rw-r--r--app/views/projects/snippets/new.html.haml1
-rw-r--r--app/views/projects/snippets/show.html.haml9
-rw-r--r--app/views/projects/tags/_download.html.haml7
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tags/destroy.js.haml1
-rw-r--r--app/views/projects/tags/index.html.haml56
-rw-r--r--app/views/projects/tags/new.html.haml1
-rw-r--r--app/views/projects/tags/show.html.haml1
-rw-r--r--app/views/projects/tree/_blob_item.html.haml5
-rw-r--r--app/views/projects/tree/_tree_item.html.haml6
-rw-r--r--app/views/projects/tree/show.html.haml21
-rw-r--r--app/views/projects/triggers/index.html.haml22
-rw-r--r--app/views/projects/variables/_content.html.haml8
-rw-r--r--app/views/projects/variables/_form.html.haml10
-rw-r--r--app/views/projects/variables/_table.html.haml25
-rw-r--r--app/views/projects/variables/index.html.haml17
-rw-r--r--app/views/projects/variables/show.html.haml41
-rw-r--r--app/views/projects/wikis/_header_title.html.haml1
-rw-r--r--app/views/projects/wikis/_main_links.html.haml9
-rw-r--r--app/views/projects/wikis/_nav.html.haml12
-rw-r--r--app/views/projects/wikis/_new.html.haml31
-rw-r--r--app/views/projects/wikis/edit.html.haml33
-rw-r--r--app/views/projects/wikis/empty.html.haml1
-rw-r--r--app/views/projects/wikis/git_access.html.haml53
-rw-r--r--app/views/projects/wikis/history.html.haml67
-rw-r--r--app/views/projects/wikis/pages.html.haml19
-rw-r--r--app/views/projects/wikis/show.html.haml35
-rw-r--r--app/views/search/results/_blob.html.haml5
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml23
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml7
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml14
-rw-r--r--app/views/shared/_labels_row.html.haml10
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml3
-rw-r--r--app/views/shared/_ref_switcher.html.haml9
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/groups/_group.html.haml6
-rw-r--r--app/views/shared/groups/_list.html.haml2
-rw-r--r--app/views/shared/icons/_activity.svg16
-rw-r--r--app/views/shared/icons/_commits.svg10
-rw-r--r--app/views/shared/icons/_contributionanalytics.svg17
-rw-r--r--app/views/shared/icons/_files.svg17
-rw-r--r--app/views/shared/icons/_group.svg18
-rw-r--r--app/views/shared/icons/_issues.svg.erb4
-rw-r--r--app/views/shared/icons/_members.svg13
-rw-r--r--app/views/shared/icons/_milestones.svg15
-rw-r--r--app/views/shared/icons/_mr.svg13
-rw-r--r--app/views/shared/icons/_pipelines.svg10
-rw-r--r--app/views/shared/icons/_project.svg10
-rw-r--r--app/views/shared/icons/_wiki.svg10
-rw-r--r--app/views/shared/issuable/_filter.html.haml10
-rw-r--r--app/views/shared/issuable/_form.html.haml97
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml17
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml12
-rw-r--r--app/views/shared/issuable/_search_form.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml78
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml10
-rw-r--r--app/views/shared/members/_member.html.haml80
-rw-r--r--app/views/shared/members/_requests.html.haml8
-rw-r--r--app/views/shared/milestones/_issuable.html.haml9
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml8
-rw-r--r--app/views/shared/milestones/_milestone.html.haml8
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_summary.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml25
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml31
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml13
-rw-r--r--app/views/shared/projects/_dropdown.html.haml17
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/shared/projects/_project.html.haml5
-rw-r--r--app/views/shared/snippets/_header.html.haml23
-rw-r--r--app/views/shared/web_hooks/_form.html.haml91
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml6
-rw-r--r--app/views/sherlock/queries/_general.html.haml8
-rw-r--r--app/views/snippets/_actions.html.haml38
-rw-r--r--app/views/snippets/show.html.haml7
-rw-r--r--app/views/u2f/_authenticate.html.haml28
-rw-r--r--app/views/u2f/_register.html.haml38
-rw-r--r--app/views/users/calendar.html.haml19
-rw-r--r--app/views/users/calendar_activities.html.haml42
-rw-r--r--app/views/users/show.atom.builder4
-rw-r--r--app/views/users/show.html.haml23
-rw-r--r--app/views/votes/_votes_block.html.haml30
-rw-r--r--app/workers/emails_on_push_worker.rb56
-rw-r--r--app/workers/expire_build_artifacts_worker.rb13
-rw-r--r--app/workers/gitlab_remove_project_export_worker.rb9
-rw-r--r--app/workers/post_receive.rb6
-rw-r--r--app/workers/project_export_worker.rb12
-rw-r--r--app/workers/repository_check/single_repository_worker.rb6
-rw-r--r--app/workers/repository_fork_worker.rb8
-rw-r--r--app/workers/repository_import_worker.rb3
-rw-r--r--app/workers/stuck_ci_builds_worker.rb2
-rwxr-xr-xbin/spring2
-rw-r--r--config/application.rb10
-rw-r--r--config/boot.rb2
-rw-r--r--config/dependency_decisions.yml189
-rw-r--r--config/environments/development.rb4
-rw-r--r--config/environments/test.rb2
-rw-r--r--config/gitlab.teatro.yml6
-rw-r--r--config/gitlab.yml.example27
-rw-r--r--config/initializers/1_settings.rb65
-rw-r--r--config/initializers/6_validations.rb24
-rw-r--r--config/initializers/carrierwave.rb4
-rw-r--r--config/initializers/chronic_duration.rb1
-rw-r--r--config/initializers/default_url_options.rb1
-rw-r--r--config/initializers/devise.rb4
-rw-r--r--config/initializers/devise_async.rb1
-rw-r--r--config/initializers/doorkeeper.rb6
-rw-r--r--config/initializers/gitlab_shell_secret_token.rb20
-rw-r--r--config/initializers/haml.rb7
-rw-r--r--config/initializers/hamlit.rb18
-rw-r--r--config/initializers/health_check.rb17
-rw-r--r--config/initializers/inflections.rb4
-rw-r--r--config/initializers/metrics.rb47
-rw-r--r--config/initializers/monkey_patch.rb48
-rw-r--r--config/initializers/omniauth.rb2
-rw-r--r--config/initializers/premailer.rb2
-rw-r--r--config/initializers/rack_attack.rb.example3
-rw-r--r--config/initializers/session_store.rb2
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/initializers/smtp_settings.rb.sample1
-rw-r--r--config/initializers/trusted_proxies.rb13
-rw-r--r--config/license_finder.yml2
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/routes.rb149
-rw-r--r--db/fixtures/development/14_builds.rb2
-rw-r--r--db/fixtures/development/15_award_emoji.rb33
-rw-r--r--db/fixtures/production/001_admin.rb12
-rw-r--r--db/migrate/20121220064453_init_schema.rb1
-rw-r--r--db/migrate/20130102143055_rename_owner_to_creator_for_project.rb1
-rw-r--r--db/migrate/20130110172407_add_public_to_project.rb1
-rw-r--r--db/migrate/20130123114545_add_issues_tracker_to_project.rb1
-rw-r--r--db/migrate/20130125090214_add_user_permissions.rb1
-rw-r--r--db/migrate/20130131070232_remove_private_flag_from_project.rb1
-rw-r--r--db/migrate/20130206084024_add_description_to_namsespace.rb1
-rw-r--r--db/migrate/20130207104426_add_description_to_teams.rb1
-rw-r--r--db/migrate/20130211085435_add_issues_tracker_id_to_project.rb1
-rw-r--r--db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb1
-rw-r--r--db/migrate/20130218140952_add_state_to_issue.rb1
-rw-r--r--db/migrate/20130218141038_add_state_to_merge_request.rb1
-rw-r--r--db/migrate/20130218141117_add_state_to_milestone.rb1
-rw-r--r--db/migrate/20130218141258_convert_closed_to_state_in_issue.rb1
-rw-r--r--db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb1
-rw-r--r--db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb1
-rw-r--r--db/migrate/20130218141444_remove_merged_from_merge_request.rb1
-rw-r--r--db/migrate/20130218141507_remove_closed_from_issue.rb1
-rw-r--r--db/migrate/20130218141536_remove_closed_from_merge_request.rb1
-rw-r--r--db/migrate/20130218141554_remove_closed_from_milestone.rb1
-rw-r--r--db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb1
-rw-r--r--db/migrate/20130220125544_convert_merge_status_in_merge_request.rb1
-rw-r--r--db/migrate/20130220125545_remove_merge_status_from_merge_request.rb1
-rw-r--r--db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb1
-rw-r--r--db/migrate/20130304104623_add_state_to_user.rb1
-rw-r--r--db/migrate/20130304104740_convert_blocked_to_state.rb1
-rw-r--r--db/migrate/20130304105317_remove_blocked_from_user.rb1
-rw-r--r--db/migrate/20130315124931_user_color_scheme.rb1
-rw-r--r--db/migrate/20130318212250_add_snippets_to_features.rb1
-rw-r--r--db/migrate/20130319214458_create_forked_project_links.rb1
-rw-r--r--db/migrate/20130323174317_add_private_to_snippets.rb1
-rw-r--r--db/migrate/20130324151736_add_type_to_snippets.rb1
-rw-r--r--db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb1
-rw-r--r--db/migrate/20130324203535_add_type_value_for_snippets.rb1
-rw-r--r--db/migrate/20130325173941_add_notification_level_to_user.rb1
-rw-r--r--db/migrate/20130326142630_add_index_to_users_authentication_token.rb1
-rw-r--r--db/migrate/20130403003950_add_last_activity_column_into_project.rb1
-rw-r--r--db/migrate/20130404164628_add_notification_level_to_user_project.rb1
-rw-r--r--db/migrate/20130410175022_remove_wiki_table.rb1
-rw-r--r--db/migrate/20130419190306_allow_merges_for_forks.rb1
-rw-r--r--db/migrate/20130506085413_add_type_to_key.rb1
-rw-r--r--db/migrate/20130506090604_create_deploy_keys_projects.rb1
-rw-r--r--db/migrate/20130506095501_remove_project_id_from_key.rb1
-rw-r--r--db/migrate/20130522141856_add_more_fields_to_service.rb1
-rw-r--r--db/migrate/20130528184641_add_system_to_notes.rb1
-rw-r--r--db/migrate/20130611210815_increase_snippet_text_column_size.rb1
-rw-r--r--db/migrate/20130613165816_add_password_expires_at_to_users.rb1
-rw-r--r--db/migrate/20130613173246_add_created_by_id_to_user.rb1
-rw-r--r--db/migrate/20130614132337_add_improted_to_project.rb1
-rw-r--r--db/migrate/20130617095603_create_users_groups.rb1
-rw-r--r--db/migrate/20130621195223_add_notification_level_to_user_group.rb1
-rw-r--r--db/migrate/20130622115340_add_more_db_index.rb1
-rw-r--r--db/migrate/20130624162710_add_fingerprint_to_key.rb1
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb1
-rw-r--r--db/migrate/20130804151314_add_st_diff_to_note.rb1
-rw-r--r--db/migrate/20130809124851_add_permission_check_to_user.rb1
-rw-r--r--db/migrate/20130812143708_add_import_url_to_project.rb1
-rw-r--r--db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb1
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb1
-rw-r--r--db/migrate/20130821090530_remove_deprecated_tables.rb1
-rw-r--r--db/migrate/20130821090531_add_internal_ids_to_milestones.rb1
-rw-r--r--db/migrate/20130909132950_add_description_to_merge_request.rb1
-rw-r--r--db/migrate/20130926081215_change_owner_id_for_group.rb1
-rw-r--r--db/migrate/20131005191208_add_avatar_to_users.rb1
-rw-r--r--db/migrate/20131009115346_add_confirmable_to_users.rb1
-rw-r--r--db/migrate/20131106151520_remove_default_branch.rb1
-rw-r--r--db/migrate/20131112114325_create_broadcast_messages.rb1
-rw-r--r--db/migrate/20131112220935_add_visibility_level_to_projects.rb1
-rw-r--r--db/migrate/20131129154016_add_archived_to_projects.rb1
-rw-r--r--db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb1
-rw-r--r--db/migrate/20131202192556_add_event_fields_for_web_hook.rb1
-rw-r--r--db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb1
-rw-r--r--db/migrate/20131217102743_add_recipients_to_service.rb1
-rw-r--r--db/migrate/20140116231608_add_website_url_to_users.rb1
-rw-r--r--db/migrate/20140122112253_create_merge_request_diffs.rb1
-rw-r--r--db/migrate/20140122114406_migrate_mr_diffs.rb1
-rw-r--r--db/migrate/20140122122549_remove_m_rdiff_fields.rb1
-rw-r--r--db/migrate/20140125162722_add_avatar_to_projects.rb1
-rw-r--r--db/migrate/20140127170938_add_group_avatars.rb1
-rw-r--r--db/migrate/20140209025651_create_emails.rb1
-rw-r--r--db/migrate/20140214102325_add_api_key_to_services.rb1
-rw-r--r--db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb1
-rw-r--r--db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb1
-rw-r--r--db/migrate/20140312145357_add_import_status_to_project.rb1
-rw-r--r--db/migrate/20140313092127_migrate_already_imported_projects.rb1
-rw-r--r--db/migrate/20140407135544_fix_namespaces.rb1
-rw-r--r--db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb1
-rw-r--r--db/migrate/20140415124820_limits_to_mysql.rb1
-rw-r--r--db/migrate/20140416074002_add_index_on_iid.rb1
-rw-r--r--db/migrate/20140416185734_index_on_current_sign_in_at.rb1
-rw-r--r--db/migrate/20140428105831_add_notes_index_updated_at.rb1
-rw-r--r--db/migrate/20140502115131_add_repo_size_to_db.rb1
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb1
-rw-r--r--db/migrate/20140611135229_add_position_to_merge_request.rb1
-rw-r--r--db/migrate/20140625115202_create_users_star_projects.rb1
-rw-r--r--db/migrate/20140729134820_create_labels.rb1
-rw-r--r--db/migrate/20140729140420_create_label_links.rb1
-rw-r--r--db/migrate/20140729145339_migrate_project_tags.rb1
-rw-r--r--db/migrate/20140729152420_migrate_taggable_labels.rb1
-rw-r--r--db/migrate/20140730111702_add_index_to_labels.rb1
-rw-r--r--db/migrate/20140903115954_migrate_to_new_shell.rb1
-rw-r--r--db/migrate/20140907220153_serialize_service_properties.rb1
-rw-r--r--db/migrate/20140914113604_add_members_table.rb1
-rw-r--r--db/migrate/20140914145549_migrate_to_new_members_model.rb1
-rw-r--r--db/migrate/20140914173417_remove_old_member_tables.rb1
-rw-r--r--db/migrate/20141006143943_move_slack_service_to_webhook.rb1
-rw-r--r--db/migrate/20141007100818_add_visibility_level_to_snippet.rb1
-rw-r--r--db/migrate/20141118150935_add_audit_event.rb1
-rw-r--r--db/migrate/20141121133009_add_timestamps_to_members.rb1
-rw-r--r--db/migrate/20141121161704_add_identity_table.rb1
-rw-r--r--db/migrate/20141205134006_add_locked_at_to_merge_request.rb1
-rw-r--r--db/migrate/20141216155758_create_doorkeeper_tables.rb1
-rw-r--r--db/migrate/20141217125223_add_owner_to_application.rb1
-rw-r--r--db/migrate/20141223135007_add_import_data_to_project_table.rb1
-rw-r--r--db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb1
-rw-r--r--db/migrate/20150108073740_create_application_settings.rb1
-rw-r--r--db/migrate/20150116234544_add_home_page_url_for_application_settings.rb1
-rw-r--r--db/migrate/20150116234545_add_gitlab_access_token_to_user.rb1
-rw-r--r--db/migrate/20150125163100_add_default_branch_protection_setting.rb1
-rw-r--r--db/migrate/20150205211843_add_timestamps_to_identities.rb1
-rw-r--r--db/migrate/20150206181414_add_index_to_created_at.rb1
-rw-r--r--db/migrate/20150206222854_add_notification_email_to_user.rb1
-rw-r--r--db/migrate/20150209222013_add_missing_index.rb1
-rw-r--r--db/migrate/20150211172122_add_template_to_service.rb1
-rw-r--r--db/migrate/20150211174341_allow_null_in_services_project_id.rb1
-rw-r--r--db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb1
-rw-r--r--db/migrate/20150213114800_add_hide_no_password_to_user.rb1
-rw-r--r--db/migrate/20150213121042_add_password_automatically_set_to_user.rb1
-rw-r--r--db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb1
-rw-r--r--db/migrate/20150219004514_add_events_to_services.rb1
-rw-r--r--db/migrate/20150223022001_set_missing_last_activity_at.rb1
-rw-r--r--db/migrate/20150225065047_add_note_events_to_services.rb1
-rw-r--r--db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb1
-rw-r--r--db/migrate/20150306023106_fix_namespace_duplication.rb1
-rw-r--r--db/migrate/20150306023112_add_unique_index_to_namespace.rb1
-rw-r--r--db/migrate/20150310194358_add_version_check_to_application_settings.rb1
-rw-r--r--db/migrate/20150313012111_create_subscriptions_table.rb1
-rw-r--r--db/migrate/20150320234437_add_location_to_user.rb1
-rw-r--r--db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb1
-rw-r--r--db/migrate/20150327122227_add_public_to_key.rb1
-rw-r--r--db/migrate/20150327150017_add_import_data_to_project.rb1
-rw-r--r--db/migrate/20150327223628_add_devise_two_factor_to_users.rb1
-rw-r--r--db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb1
-rw-r--r--db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb1
-rw-r--r--db/migrate/20150406133311_add_invite_data_to_member.rb1
-rw-r--r--db/migrate/20150411000035_fix_identities.rb1
-rw-r--r--db/migrate/20150411180045_rename_buildbox_service.rb1
-rw-r--r--db/migrate/20150413192223_add_public_email_to_users.rb1
-rw-r--r--db/migrate/20150417121913_create_project_import_data.rb1
-rw-r--r--db/migrate/20150417122318_remove_import_data_from_project.rb1
-rw-r--r--db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb1
-rw-r--r--db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb1
-rw-r--r--db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb1
-rw-r--r--db/migrate/20150425164647_remove_duplicate_tags.rb1
-rw-r--r--db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb1
-rw-r--r--db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb1
-rw-r--r--db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb1
-rw-r--r--db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb1
-rw-r--r--db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb1
-rw-r--r--db/migrate/20150429002313_remove_abandoned_group_members_records.rb1
-rw-r--r--db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb1
-rw-r--r--db/migrate/20150509180749_convert_legacy_reference_notes.rb1
-rw-r--r--db/migrate/20150516060434_add_note_events_to_web_hooks.rb1
-rw-r--r--db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb1
-rw-r--r--db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb1
-rw-r--r--db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb1
-rw-r--r--db/migrate/20150610065936_add_dashboard_to_users.rb1
-rw-r--r--db/migrate/20150620233230_add_default_otp_required_for_login_value.rb1
-rw-r--r--db/migrate/20150713160110_add_project_view_to_users.rb1
-rw-r--r--db/migrate/20150717130904_add_commits_count_to_project.rb1
-rw-r--r--db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb1
-rw-r--r--db/migrate/20150806104937_create_abuse_reports.rb1
-rw-r--r--db/migrate/20150812080800_add_settings_import_sources.rb1
-rw-r--r--db/migrate/20150814065925_remove_oauth_tokens_from_users.rb1
-rw-r--r--db/migrate/20150817163600_deduplicate_user_identities.rb1
-rw-r--r--db/migrate/20150818213832_add_sent_notifications.rb1
-rw-r--r--db/migrate/20150824002011_add_enable_ssl_verification.rb1
-rw-r--r--db/migrate/20150826001931_add_ci_tables.rb1
-rw-r--r--db/migrate/20150902001023_add_template_to_label.rb1
-rw-r--r--db/migrate/20150914215247_add_ci_tags.rb1
-rw-r--r--db/migrate/20150915001905_enable_ssl_verification_by_default.rb1
-rw-r--r--db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb1
-rw-r--r--db/migrate/20150916114643_add_help_page_text_to_application_settings.rb1
-rw-r--r--db/migrate/20150916145038_add_index_for_committed_at_and_id.rb1
-rw-r--r--db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb1
-rw-r--r--db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb1
-rw-r--r--db/migrate/20150920010715_add_consumed_timestep_to_users.rb1
-rw-r--r--db/migrate/20150920161119_add_line_code_to_sent_notification.rb1
-rw-r--r--db/migrate/20150924125150_add_project_id_to_ci_commit.rb1
-rw-r--r--db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb1
-rw-r--r--db/migrate/20150930001110_merge_request_error_field.rb1
-rw-r--r--db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb1
-rw-r--r--db/migrate/20150930110012_add_group_share_lock.rb1
-rw-r--r--db/migrate/20151002112914_add_stage_idx_to_builds.rb1
-rw-r--r--db/migrate/20151002121400_add_index_for_builds.rb1
-rw-r--r--db/migrate/20151002122929_add_ref_and_tag_to_builds.rb1
-rw-r--r--db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb1
-rw-r--r--db/migrate/20151005075649_add_user_id_to_build.rb1
-rw-r--r--db/migrate/20151005150751_add_layout_option_for_users.rb1
-rw-r--r--db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb1
-rw-r--r--db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb1
-rw-r--r--db/migrate/20151008110232_add_users_lower_username_email_indexes.rb1
-rw-r--r--db/migrate/20151008123042_add_type_and_description_to_builds.rb1
-rw-r--r--db/migrate/20151008130321_migrate_name_to_description_for_builds.rb1
-rw-r--r--db/migrate/20151008143519_add_admin_notification_email_setting.rb1
-rw-r--r--db/migrate/20151012173029_set_jira_service_api_url.rb1
-rw-r--r--db/migrate/20151013092124_add_artifacts_file_to_builds.rb1
-rw-r--r--db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb1
-rw-r--r--db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb1
-rw-r--r--db/migrate/20151016195706_add_notes_line_code_index.rb1
-rw-r--r--db/migrate/20151019111551_fix_build_tags.rb1
-rw-r--r--db/migrate/20151019111703_fail_build_without_names.rb1
-rw-r--r--db/migrate/20151020145526_add_services_template_index.rb1
-rw-r--r--db/migrate/20151020173516_ci_limits_to_mysql.rb1
-rw-r--r--db/migrate/20151020173906_add_ci_builds_index_for_status.rb1
-rw-r--r--db/migrate/20151023112551_fail_build_with_empty_name.rb1
-rw-r--r--db/migrate/20151023144219_remove_satellites.rb1
-rw-r--r--db/migrate/20151026182941_add_project_path_index.rb1
-rw-r--r--db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb1
-rw-r--r--db/migrate/20151103001141_add_public_to_group.rb1
-rw-r--r--db/migrate/20151103133339_add_shared_runners_setting.rb1
-rw-r--r--db/migrate/20151103134857_create_lfs_objects.rb1
-rw-r--r--db/migrate/20151103134958_create_lfs_objects_projects.rb1
-rw-r--r--db/migrate/20151104105513_add_file_to_lfs_objects.rb1
-rw-r--r--db/migrate/20151105094515_create_releases.rb1
-rw-r--r--db/migrate/20151106000015_add_is_award_to_notes.rb1
-rw-r--r--db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb1
-rw-r--r--db/migrate/20151109134526_add_issues_state_index.rb1
-rw-r--r--db/migrate/20151109134916_add_projects_visibility_level_index.rb1
-rw-r--r--db/migrate/20151110125604_add_import_error_to_project.rb1
-rw-r--r--db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb1
-rw-r--r--db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb1
-rw-r--r--db/migrate/20151118162244_add_projects_public_index.rb1
-rw-r--r--db/migrate/20151201203948_raise_hook_url_limit.rb1
-rw-r--r--db/migrate/20151203162133_add_hide_project_limit_to_users.rb1
-rw-r--r--db/migrate/20151203162134_add_build_events_to_services.rb1
-rw-r--r--db/migrate/20151209144329_migrate_ci_web_hooks.rb1
-rw-r--r--db/migrate/20151209145909_migrate_ci_emails.rb1
-rw-r--r--db/migrate/20151210030143_add_unlock_token_to_user.rb1
-rw-r--r--db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb1
-rw-r--r--db/migrate/20151210125232_migrate_ci_slack_service.rb1
-rw-r--r--db/migrate/20151210125927_migrate_ci_hip_chat_service.rb1
-rw-r--r--db/migrate/20151210125928_add_ci_to_project.rb1
-rw-r--r--db/migrate/20151210125929_add_project_id_to_ci.rb1
-rw-r--r--db/migrate/20151210125930_migrate_ci_to_project.rb1
-rw-r--r--db/migrate/20151210125931_add_index_to_ci_tables.rb1
-rw-r--r--db/migrate/20151210125932_drop_null_for_ci_tables.rb1
-rw-r--r--db/migrate/20151218154042_add_tfa_to_application_settings.rb1
-rw-r--r--db/migrate/20151221234414_add_tfa_additional_fields.rb1
-rw-r--r--db/migrate/20151224123230_rename_emojis.rb1
-rw-r--r--db/migrate/20151228111122_remove_public_from_namespace.rb1
-rw-r--r--db/migrate/20151228150906_influxdb_settings.rb1
-rw-r--r--db/migrate/20151228175719_add_recaptcha_to_application_settings.rb1
-rw-r--r--db/migrate/20151229102248_influxdb_udp_port_setting.rb1
-rw-r--r--db/migrate/20151229112614_influxdb_remote_database_setting.rb1
-rw-r--r--db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb1
-rw-r--r--db/migrate/20151231152326_add_akismet_to_application_settings.rb1
-rw-r--r--db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb1
-rw-r--r--db/migrate/20160106162223_add_index_milestones_title.rb1
-rw-r--r--db/migrate/20160106164438_remove_influxdb_credentials.rb1
-rw-r--r--db/migrate/20160109054846_create_spam_logs.rb1
-rw-r--r--db/migrate/20160113111034_add_metrics_sample_interval.rb1
-rw-r--r--db/migrate/20160118155830_add_sentry_to_application_settings.rb1
-rw-r--r--db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb1
-rw-r--r--db/migrate/20160119111158_add_services_category.rb1
-rw-r--r--db/migrate/20160119112418_add_services_default.rb1
-rw-r--r--db/migrate/20160119145451_add_ldap_email_to_users.rb1
-rw-r--r--db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb1
-rw-r--r--db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb1
-rw-r--r--db/migrate/20160122185421_add_pending_delete_to_project.rb1
-rw-r--r--db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb1
-rw-r--r--db/migrate/20160128233227_change_lfs_objects_size_column.rb1
-rw-r--r--db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb1
-rw-r--r--db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb1
-rw-r--r--db/migrate/20160202091601_add_erasable_to_ci_build.rb1
-rw-r--r--db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb1
-rw-r--r--db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb1
-rw-r--r--db/migrate/20160209130428_add_index_to_snippet.rb1
-rw-r--r--db/migrate/20160212123307_create_tasks.rb1
-rw-r--r--db/migrate/20160217100506_add_description_to_label.rb1
-rw-r--r--db/migrate/20160217174422_add_note_to_tasks.rb1
-rw-r--r--db/migrate/20160220123949_rename_tasks_to_todos.rb1
-rw-r--r--db/migrate/20160222153918_create_appearances_ce.rb1
-rw-r--r--db/migrate/20160223192159_add_confidential_to_issues.rb1
-rw-r--r--db/migrate/20160225090018_add_delete_at_to_issues.rb1
-rw-r--r--db/migrate/20160225101956_add_delete_at_to_merge_requests.rb1
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb1
-rw-r--r--db/migrate/20160227120001_add_event_field_for_web_hook.rb1
-rw-r--r--db/migrate/20160227120047_add_event_to_services.rb1
-rw-r--r--db/migrate/20160229193553_add_main_language_to_repository.rb1
-rw-r--r--db/migrate/20160301124843_add_visibility_level_to_groups.rb1
-rw-r--r--db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb1
-rw-r--r--db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb5
-rw-r--r--db/migrate/20160305220806_remove_expires_at_from_snippets.rb1
-rw-r--r--db/migrate/20160307221555_disallow_blank_line_code_on_note.rb1
-rw-r--r--db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb5
-rw-r--r--db/migrate/20160309140734_fix_todos.rb1
-rw-r--r--db/migrate/20160310124959_add_due_date_to_issues.rb1
-rw-r--r--db/migrate/20160310185910_add_external_flag_to_users.rb1
-rw-r--r--db/migrate/20160314094147_add_priority_to_label.rb7
-rw-r--r--db/migrate/20160314114439_add_requested_at_to_members.rb5
-rw-r--r--db/migrate/20160314143402_projects_add_pushes_since_gc.rb1
-rw-r--r--db/migrate/20160315135439_project_add_repository_check.rb1
-rw-r--r--db/migrate/20160316123110_ci_runners_token_index.rb1
-rw-r--r--db/migrate/20160316192622_change_target_id_to_null_on_todos.rb1
-rw-r--r--db/migrate/20160316204731_add_commit_id_to_todos.rb1
-rw-r--r--db/migrate/20160317092222_add_moved_to_to_issue.rb1
-rw-r--r--db/migrate/20160320204112_index_namespaces_on_visibility_level.rb1
-rw-r--r--db/migrate/20160324020319_remove_todos_for_deleted_issues.rb1
-rw-r--r--db/migrate/20160328112808_create_notification_settings.rb1
-rw-r--r--db/migrate/20160328115649_migrate_new_notification_setting.rb1
-rw-r--r--db/migrate/20160328121138_add_notification_setting_index.rb1
-rw-r--r--db/migrate/20160329144452_add_index_on_pending_delete_projects.rb1
-rw-r--r--db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb1
-rw-r--r--db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb1
-rw-r--r--db/migrate/20160407120251_add_images_enabled_for_project.rb6
-rw-r--r--db/migrate/20160412140240_add_repository_checks_enabled_setting.rb1
-rw-r--r--db/migrate/20160412173416_add_fields_to_ci_commit.rb1
-rw-r--r--db/migrate/20160412173417_update_ci_commit.rb1
-rw-r--r--db/migrate/20160412173418_add_ci_commit_indexes.rb1
-rw-r--r--db/migrate/20160413115152_add_token_to_web_hooks.rb1
-rw-r--r--db/migrate/20160415062917_create_personal_access_tokens.rb13
-rw-r--r--db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb1
-rw-r--r--db/migrate/20160416180807_add_award_emoji.rb15
-rw-r--r--db/migrate/20160416182152_convert_award_note_to_emoji_award.rb37
-rw-r--r--db/migrate/20160419120017_add_metrics_packet_size.rb1
-rw-r--r--db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb15
-rw-r--r--db/migrate/20160421130527_disable_repository_checks.rb1
-rw-r--r--db/migrate/20160425045124_create_u2f_registrations.rb14
-rw-r--r--db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb6
-rw-r--r--db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb14
-rw-r--r--db/migrate/20160508194200_remove_wall_enabled_from_projects.rb1
-rw-r--r--db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb5
-rw-r--r--db/migrate/20160508215820_add_type_to_notes.rb6
-rw-r--r--db/migrate/20160508215920_add_positions_to_diff_notes.rb6
-rw-r--r--db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb6
-rw-r--r--db/migrate/20160509091049_add_locked_to_ci_runner.rb13
-rw-r--r--db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb6
-rw-r--r--db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb13
-rw-r--r--db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb5
-rw-r--r--db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb5
-rw-r--r--db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb22
-rw-r--r--db/migrate/20160525205328_remove_main_language_from_projects.rb22
-rw-r--r--db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb14
-rw-r--r--db/migrate/20160528043124_add_users_state_index.rb10
-rw-r--r--db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb10
-rw-r--r--db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb10
-rw-r--r--db/migrate/20160603180330_remove_duplicated_notification_settings.rb33
-rw-r--r--db/migrate/20160603182247_add_index_to_notification_settings.rb10
-rw-r--r--db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb6
-rw-r--r--db/migrate/20160608195742_add_repository_storage_to_projects.rb12
-rw-r--r--db/migrate/20160608211215_add_user_default_external_to_application_settings.rb13
-rw-r--r--db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb11
-rw-r--r--db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb6
-rw-r--r--db/migrate/20160610201627_migrate_users_notification_level.rb21
-rw-r--r--db/migrate/20160610204157_add_deployments.rb27
-rw-r--r--db/migrate/20160610204158_add_environments.rb17
-rw-r--r--db/migrate/20160610211845_add_environment_to_builds.rb10
-rw-r--r--db/migrate/20160610301627_remove_notification_level_from_users.rb7
-rw-r--r--db/migrate/20160614182521_add_repository_storage_to_application_settings.rb5
-rw-r--r--db/migrate/20160615142710_add_index_on_requested_at_to_members.rb9
-rw-r--r--db/migrate/20160615173316_add_enabled_git_access_protocols_to_application_settings.rb11
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb9
-rw-r--r--db/migrate/20160616084004_change_project_of_environment.rb21
-rw-r--r--db/migrate/20160616102642_remove_duplicated_keys.rb19
-rw-r--r--db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb21
-rw-r--r--db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb13
-rw-r--r--db/migrate/20160617301627_add_events_to_notification_settings.rb7
-rw-r--r--db/migrate/20160620110927_fix_no_validatable_import_url.rb106
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb12
-rw-r--r--db/migrate/20160628085157_add_artifacts_size_to_ci_builds.rb7
-rw-r--r--db/migrate/20160703180340_add_index_on_award_emoji_user_and_name.rb11
-rw-r--r--db/migrate/20160705163108_remove_requesters_that_are_owners.rb40
-rw-r--r--db/migrate/limits_to_mysql.rb1
-rw-r--r--db/schema.rb196
-rw-r--r--doc/README.md27
-rw-r--r--doc/administration/access_restrictions.md38
-rw-r--r--doc/administration/auth/ldap.md42
-rw-r--r--doc/administration/container_registry.md457
-rw-r--r--doc/administration/custom_hooks.md56
-rw-r--r--doc/administration/environment_variables.md2
-rw-r--r--doc/administration/high_availability/README.md4
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/high_availability/nfs.md6
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/img/access_restrictions.pngbin0 -> 317529 bytes
-rw-r--r--doc/administration/img/custom_hooks_error_msg.pngbin0 -> 159486 bytes
-rw-r--r--doc/administration/img/high_availability/active-active-diagram.pngbin0 -> 29607 bytes
-rw-r--r--doc/administration/img/high_availability/active-passive-diagram.pngbin0 -> 24246 bytes
-rw-r--r--doc/administration/img/housekeeping_settings.pngbin23856 -> 19347 bytes
-rw-r--r--doc/administration/img/restricted_url.pngbin0 -> 188210 bytes
-rw-r--r--doc/administration/logs.md137
-rw-r--r--doc/administration/raketasks/project_import_export.md33
-rw-r--r--doc/administration/repository_checks.md4
-rw-r--r--doc/administration/repository_storages.md18
-rw-r--r--doc/administration/troubleshooting/debug.md169
-rw-r--r--doc/administration/troubleshooting/gdb-stuck-ruby.txt142
-rw-r--r--doc/administration/troubleshooting/sidekiq.md11
-rw-r--r--doc/api/README.md111
-rw-r--r--doc/api/award_emoji.md367
-rw-r--r--doc/api/builds.md557
-rw-r--r--doc/api/ci/README.md24
-rw-r--r--doc/api/ci/builds.md138
-rw-r--r--doc/api/ci/runners.md57
-rw-r--r--doc/api/groups.md249
-rw-r--r--doc/api/issues.md176
-rw-r--r--doc/api/labels.md72
-rw-r--r--doc/api/merge_requests.md122
-rw-r--r--doc/api/oauth2.md9
-rw-r--r--doc/api/projects.md42
-rw-r--r--doc/api/runners.md4
-rw-r--r--doc/api/services.md44
-rw-r--r--doc/api/session.md9
-rw-r--r--doc/api/settings.md13
-rw-r--r--doc/api/sidekiq_metrics.md152
-rw-r--r--doc/api/todos.md444
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/api/README.md21
-rw-r--r--doc/ci/api/builds.md138
-rw-r--r--doc/ci/api/runners.md45
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser.pngbin89132 -> 82102 bytes
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser_button.pngbin11614 -> 7230 bytes
-rw-r--r--doc/ci/docker/using_docker_build.md224
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/environments.md58
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/deployment/README.md (renamed from doc/ci/deployment/README.md)0
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/examples/test-scala-application.md47
-rw-r--r--doc/ci/img/builds_tab.pngbin3845 -> 3047 bytes
-rw-r--r--doc/ci/img/features_settings.pngbin18691 -> 15809 bytes
-rw-r--r--doc/ci/pipelines.md38
-rw-r--r--doc/ci/quick_start/README.md68
-rw-r--r--doc/ci/quick_start/img/build_log.pngbin63272 -> 52482 bytes
-rw-r--r--doc/ci/quick_start/img/builds_status.pngbin49121 -> 41838 bytes
-rw-r--r--doc/ci/quick_start/img/new_commit.pngbin9033 -> 7587 bytes
-rw-r--r--doc/ci/quick_start/img/pipelines_status.pngbin0 -> 89387 bytes
-rw-r--r--doc/ci/quick_start/img/runners_activated.pngbin27597 -> 22822 bytes
-rw-r--r--doc/ci/quick_start/img/single_commit_status_pending.pngbin36431 -> 29981 bytes
-rw-r--r--doc/ci/quick_start/img/status_pending.pngbin19782 -> 16205 bytes
-rw-r--r--doc/ci/runners/README.md20
-rw-r--r--doc/ci/runners/project_specific.pngbin31408 -> 30196 bytes
-rw-r--r--doc/ci/runners/shared_runner.pngbin18366 -> 17797 bytes
-rw-r--r--doc/ci/runners/shared_to_specific_admin.pngbin5897 -> 5715 bytes
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin39713 -> 33324 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin2895 -> 2387 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin5418 -> 4433 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin15889 -> 12943 bytes
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md231
-rw-r--r--doc/container_registry/README.md94
-rw-r--r--doc/container_registry/img/container_registry.pngbin0 -> 222782 bytes
-rw-r--r--doc/container_registry/img/project_feature.pngbin0 -> 248750 bytes
-rw-r--r--doc/customization/branded_login_page/appearance.pngbin365120 -> 156228 bytes
-rw-r--r--doc/customization/branded_login_page/custom_sign_in.pngbin314111 -> 166674 bytes
-rw-r--r--doc/customization/branded_login_page/default_login_page.pngbin292731 -> 150538 bytes
-rw-r--r--doc/customization/issue_closing.md2
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/development/ci_setup.md2
-rw-r--r--doc/development/doc_styleguide.md112
-rw-r--r--doc/development/gitlab_architecture_diagram.pngbin0 -> 23831 bytes
-rw-r--r--doc/development/gitlab_diagram_overview.pngbin256612 -> 0 bytes
-rw-r--r--doc/development/gotchas.md2
-rw-r--r--doc/development/instrumentation.md22
-rw-r--r--doc/development/licensing.md93
-rw-r--r--doc/development/migration_style_guide.md61
-rw-r--r--doc/development/rake_tasks.md20
-rw-r--r--doc/development/testing.md2
-rw-r--r--doc/development/ui_guide.md48
-rw-r--r--doc/downgrade_ee_to_ce/README.md4
-rw-r--r--doc/gitlab-basics/basicsimages/add_new_merge_request.pngbin9467 -> 9003 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/add_sshkey.pngbin1463 -> 1394 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_info.pngbin7978 -> 7572 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_name.pngbin2199 -> 2137 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branches.pngbin3653 -> 3548 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/button-create-mr.pngbin6154 -> 5927 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/click-on-new-group.pngbin2063 -> 1957 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_changes.pngbin5567 -> 4941 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_message.pngbin5707 -> 5103 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commits.pngbin4258 -> 4112 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.pngbin1624 -> 1520 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_file.pngbin2524 -> 2451 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_group.pngbin3224 -> 3184 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/edit_file.pngbin2259 -> 2221 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_located.pngbin3156 -> 3078 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_name.pngbin2544 -> 2412 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_file.pngbin8840 -> 8426 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_group.pngbin6159 -> 5897 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/fork.pngbin1046 -> 896 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/group_info.pngbin16217 -> 15479 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/groups.pngbin4857 -> 4752 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/https.pngbin2887 -> 2822 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/image_file.pngbin2939 -> 2796 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issue_title.pngbin9059 -> 8311 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issues.pngbin4332 -> 4153 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/key.pngbin1264 -> 1177 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/merge_requests.pngbin4381 -> 4213 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_merge_request.pngbin3227 -> 3162 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_project.pngbin2319 -> 2234 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/newbranch.pngbin1314 -> 1244 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/paste_sshkey.pngbin8620 -> 7699 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/profile_settings.pngbin1194 -> 1101 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/project_info.pngbin21862 -> 21041 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/public_file_link.pngbin3038 -> 3023 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group.pngbin6075 -> 6034 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group2.pngbin5049 -> 5040 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_branch.pngbin12213 -> 11207 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_project.pngbin16832 -> 16176 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/settings.pngbin4321 -> 4149 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/shh_keys.pngbin4981 -> 4782 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/submit_new_issue.pngbin9083 -> 8644 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/title_description_mr.pngbin12749 -> 11919 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/white_space.pngbin3707 -> 2192 bytes
-rw-r--r--doc/gitlab-basics/create-issue.md2
-rw-r--r--doc/gitlab-basics/create-project.md2
-rw-r--r--doc/hooks/custom_hooks.md40
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/install/relative_url.md2
-rw-r--r--doc/install/requirements.md5
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/cas.md19
-rw-r--r--doc/integration/external-issue-tracker.md3
-rw-r--r--doc/integration/google.md4
-rw-r--r--doc/integration/img/akismet_settings.pngbin55837 -> 26625 bytes
-rw-r--r--doc/integration/img/enabled-oauth-sign-in-sources.pngbin0 -> 21767 bytes
-rw-r--r--doc/integration/img/facebook_api_keys.pngbin125921 -> 85832 bytes
-rw-r--r--doc/integration/img/facebook_app_settings.pngbin134387 -> 68086 bytes
-rw-r--r--doc/integration/img/facebook_website_url.pngbin42292 -> 19823 bytes
-rw-r--r--doc/integration/img/github_app.pngbin75297 -> 55591 bytes
-rw-r--r--doc/integration/img/gitlab_app.pngbin55325 -> 30963 bytes
-rw-r--r--doc/integration/img/gmail_action_buttons_for_gitlab.pngbin17321 -> 16020 bytes
-rw-r--r--doc/integration/img/google_app.pngbin52669 -> 29154 bytes
-rw-r--r--doc/integration/img/oauth_provider_admin_application.pngbin40579 -> 33440 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_form.pngbin27974 -> 23048 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_id_secret.pngbin33901 -> 27673 bytes
-rw-r--r--doc/integration/img/oauth_provider_authorized_application.pngbin32225 -> 26622 bytes
-rw-r--r--doc/integration/img/oauth_provider_user_wide_applications.pngbin40632 -> 33337 bytes
-rw-r--r--doc/integration/img/twitter_app_api_keys.pngbin72200 -> 36921 bytes
-rw-r--r--doc/integration/img/twitter_app_details.pngbin121621 -> 64686 bytes
-rw-r--r--doc/integration/omniauth.md27
-rw-r--r--doc/integration/shibboleth.md2
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/logs/logs.md93
-rw-r--r--doc/markdown/img/logo.pngbin11097 -> 9509 bytes
-rw-r--r--doc/markdown/markdown.md49
-rw-r--r--doc/migrate_ci_to_ce/README.md2
-rw-r--r--doc/monitoring/health_check.md66
-rw-r--r--doc/monitoring/img/health_check_token.pngbin0 -> 6630 bytes
-rw-r--r--doc/monitoring/performance/grafana_configuration.md74
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_dropdown.pngbin29419 -> 14368 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_import.pngbin40974 -> 18267 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_configuration.pngbin53402 -> 26060 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_empty.pngbin44058 -> 21821 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_save_icon.pngbin16024 -> 9107 bytes
-rw-r--r--doc/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin45148 -> 37228 bytes
-rw-r--r--doc/operations/moving_repositories.md8
-rw-r--r--doc/permissions/permissions.md9
-rw-r--r--doc/profile/2fa.pngbin23415 -> 22047 bytes
-rw-r--r--doc/profile/2fa_auth.pngbin15569 -> 14535 bytes
-rw-r--r--doc/profile/2fa_u2f_authenticate.pngbin0 -> 54413 bytes
-rw-r--r--doc/profile/2fa_u2f_register.pngbin0 -> 112414 bytes
-rw-r--r--doc/profile/two_factor_authentication.md63
-rw-r--r--doc/project_services/bugzilla.md17
-rw-r--r--doc/project_services/emails_on_push.md17
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin41222 -> 33943 bytes
-rw-r--r--doc/project_services/img/emails_on_push_service.pngbin0 -> 98160 bytes
-rw-r--r--doc/project_services/img/jira_add_gitlab_commit_message.pngbin57136 -> 46590 bytes
-rw-r--r--doc/project_services/img/jira_add_user_to_group.pngbin59251 -> 41994 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group.pngbin41294 -> 32934 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group_name.pngbin12535 -> 9054 bytes
-rw-r--r--doc/project_services/img/jira_create_new_user.pngbin26532 -> 21081 bytes
-rw-r--r--doc/project_services/img/jira_group_access.pngbin46028 -> 32210 bytes
-rw-r--r--doc/project_services/img/jira_issue_closed.pngbin92601 -> 77028 bytes
-rw-r--r--doc/project_services/img/jira_issue_reference.pngbin39942 -> 36188 bytes
-rw-r--r--doc/project_services/img/jira_issues_workflow.pngbin105237 -> 87067 bytes
-rw-r--r--doc/project_services/img/jira_merge_request_close.pngbin111150 -> 102835 bytes
-rw-r--r--doc/project_services/img/jira_project_name.pngbin60598 -> 41572 bytes
-rw-r--r--doc/project_services/img/jira_reference_commit_message_in_jira_issue.pngbin42452 -> 33706 bytes
-rw-r--r--doc/project_services/img/jira_service.pngbin59082 -> 56834 bytes
-rw-r--r--doc/project_services/img/jira_service_close_issue.pngbin88433 -> 79569 bytes
-rw-r--r--doc/project_services/img/jira_service_page.pngbin49122 -> 36280 bytes
-rw-r--r--doc/project_services/img/jira_submit_gitlab_merge_request.pngbin63063 -> 51913 bytes
-rw-r--r--doc/project_services/img/jira_user_management_link.pngbin58211 -> 43095 bytes
-rw-r--r--doc/project_services/img/jira_workflow_screenshot.pngbin121534 -> 111093 bytes
-rw-r--r--doc/project_services/img/redmine_configuration.pngbin21061 -> 16973 bytes
-rw-r--r--doc/project_services/img/services_templates_redmine_example.pngbin17351 -> 13936 bytes
-rw-r--r--doc/project_services/project_services.md3
-rw-r--r--doc/public_access/public_access.md6
-rw-r--r--doc/raketasks/README.md2
-rw-r--r--doc/raketasks/backup_hrz.pngbin21955 -> 8907 bytes
-rw-r--r--doc/raketasks/check_repos_output.pngbin73786 -> 35333 bytes
-rw-r--r--doc/raketasks/import.md3
-rw-r--r--doc/security/README.md1
-rw-r--r--doc/security/img/two_factor_authentication_settings.pngbin20399 -> 16807 bytes
-rw-r--r--doc/security/user_email_confirmation.md7
-rw-r--r--doc/ssh/README.md10
-rw-r--r--doc/update/2.6-to-3.0.md4
-rw-r--r--doc/update/2.9-to-3.0.md5
-rw-r--r--doc/update/3.0-to-3.1.md5
-rw-r--r--doc/update/3.1-to-4.0.md5
-rw-r--r--doc/update/4.0-to-4.1.md5
-rw-r--r--doc/update/4.1-to-4.2.md10
-rw-r--r--doc/update/4.2-to-5.0.md9
-rw-r--r--doc/update/5.0-to-5.1.md10
-rw-r--r--doc/update/5.1-to-5.2.md18
-rw-r--r--doc/update/5.1-to-5.4.md18
-rw-r--r--doc/update/5.1-to-6.0.md18
-rw-r--r--doc/update/5.2-to-5.3.md18
-rw-r--r--doc/update/5.3-to-5.4.md18
-rw-r--r--doc/update/5.4-to-6.0.md16
-rw-r--r--doc/update/6.0-to-6.1.md19
-rw-r--r--doc/update/6.1-to-6.2.md19
-rw-r--r--doc/update/6.2-to-6.3.md17
-rw-r--r--doc/update/6.3-to-6.4.md17
-rw-r--r--doc/update/6.4-to-6.5.md21
-rw-r--r--doc/update/6.5-to-6.6.md20
-rw-r--r--doc/update/6.6-to-6.7.md16
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md7
-rw-r--r--doc/update/8.6-to-8.7.md10
-rw-r--r--doc/update/8.7-to-8.8.md162
-rw-r--r--doc/update/8.8-to-8.9.md191
-rw-r--r--doc/update/8.9-to-8.10.md191
-rw-r--r--doc/update/README.md6
-rw-r--r--doc/update/patch_versions.md4
-rw-r--r--doc/user/project/highlighting.md31
-rw-r--r--doc/user/project/img/labels_assign_label_in_new_issue.pngbin0 -> 31126 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar.pngbin0 -> 31537 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar_saved.pngbin0 -> 28396 bytes
-rw-r--r--doc/user/project/img/labels_default.pngbin0 -> 80403 bytes
-rw-r--r--doc/user/project/img/labels_description_tooltip.pngbin0 -> 22585 bytes
-rw-r--r--doc/user/project/img/labels_filter.pngbin0 -> 81536 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin0 -> 60849 bytes
-rw-r--r--doc/user/project/img/labels_generate.pngbin0 -> 31608 bytes
-rw-r--r--doc/user/project/img/labels_new_label.pngbin0 -> 43265 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly.pngbin0 -> 10416 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly_create.pngbin0 -> 16151 bytes
-rw-r--r--doc/user/project/img/labels_prioritize.pngbin0 -> 108751 bytes
-rw-r--r--doc/user/project/img/labels_subscribe.pngbin0 -> 11536 bytes
-rw-r--r--doc/user/project/labels.md147
-rw-r--r--doc/user/project/settings/img/import_export_download_export.pngbin0 -> 85600 bytes
-rw-r--r--doc/user/project/settings/img/import_export_export_button.pngbin0 -> 84637 bytes
-rw-r--r--doc/user/project/settings/img/import_export_mail_link.pngbin0 -> 44012 bytes
-rw-r--r--doc/user/project/settings/img/import_export_new_project.pngbin0 -> 43574 bytes
-rw-r--r--doc/user/project/settings/img/import_export_select_file.pngbin0 -> 46292 bytes
-rw-r--r--doc/user/project/settings/img/settings_edit_button.pngbin0 -> 19392 bytes
-rw-r--r--doc/user/project/settings/import_export.md73
-rw-r--r--doc/web_hooks/ssl.pngbin77165 -> 39120 bytes
-rw-r--r--doc/web_hooks/web_hooks.md68
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/add-user/add-user.md24
-rw-r--r--doc/workflow/add-user/img/access_requests_management.pngbin0 -> 15686 bytes
-rw-r--r--doc/workflow/add-user/img/add_new_user_to_project_settings.pngbin22822 -> 18149 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin10833 -> 22877 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin16177 -> 40207 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin15889 -> 45798 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin22089 -> 56380 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin18897 -> 38778 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin23897 -> 37835 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin15732 -> 24337 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.pngbin8295 -> 42224 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin13518 -> 39844 bytes
-rw-r--r--doc/workflow/add-user/img/request_access_button.pngbin0 -> 36588 bytes
-rw-r--r--doc/workflow/add-user/img/withdraw_access_request_button.pngbin0 -> 37960 bytes
-rw-r--r--doc/workflow/award_emoji.md45
-rw-r--r--doc/workflow/award_emoji.pngbin6620 -> 9939 bytes
-rw-r--r--doc/workflow/ci_mr.pngbin40065 -> 29571 bytes
-rw-r--r--doc/workflow/close_issue_mr.pngbin146292 -> 82595 bytes
-rw-r--r--doc/workflow/environment_branches.pngbin40210 -> 20745 bytes
-rw-r--r--doc/workflow/forking/branch_select.pngbin55352 -> 27299 bytes
-rw-r--r--doc/workflow/forking/merge_request.pngbin60597 -> 31560 bytes
-rw-r--r--doc/workflow/four_stages.pngbin20934 -> 10003 bytes
-rw-r--r--doc/workflow/git_pull.pngbin167056 -> 94405 bytes
-rw-r--r--doc/workflow/gitdashflow.pngbin184726 -> 131491 bytes
-rw-r--r--doc/workflow/github_flow.pngbin20600 -> 10251 bytes
-rw-r--r--doc/workflow/gitlab_flow.md4
-rw-r--r--doc/workflow/gitlab_flow.pngbin90883 -> 70871 bytes
-rw-r--r--doc/workflow/good_commit.pngbin28433 -> 13131 bytes
-rw-r--r--doc/workflow/groups.md24
-rw-r--r--doc/workflow/groups/access_requests_management.pngbin0 -> 15829 bytes
-rw-r--r--doc/workflow/groups/add_member_to_group.pngbin138184 -> 78060 bytes
-rw-r--r--doc/workflow/groups/group_dashboard.pngbin107332 -> 59446 bytes
-rw-r--r--doc/workflow/groups/group_with_two_projects.pngbin129319 -> 73101 bytes
-rw-r--r--doc/workflow/groups/max_access_level.pngbin135354 -> 74947 bytes
-rw-r--r--doc/workflow/groups/new_group_button.pngbin185406 -> 108482 bytes
-rw-r--r--doc/workflow/groups/new_group_form.pngbin106491 -> 58860 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin118382 -> 64447 bytes
-rw-r--r--doc/workflow/groups/override_access_level.pngbin157193 -> 90122 bytes
-rw-r--r--doc/workflow/groups/project_members_via_group.pngbin151339 -> 86260 bytes
-rw-r--r--doc/workflow/groups/request_access_button.pngbin0 -> 49067 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin118868 -> 65633 bytes
-rw-r--r--doc/workflow/groups/transfer_project.pngbin164958 -> 92115 bytes
-rw-r--r--doc/workflow/groups/withdraw_access_request_button.pngbin0 -> 49941 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_awarded.pngbin0 -> 64317 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_picker.pngbin0 -> 250861 bytes
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin65985 -> 49296 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin144501 -> 116715 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin136577 -> 108775 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin162251 -> 131659 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit.pngbin353067 -> 304098 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit_modal.pngbin312659 -> 264883 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr.pngbin252085 -> 212267 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr_modal.pngbin225569 -> 186597 bytes
-rw-r--r--doc/workflow/img/file_finder_find_button.pngbin30974 -> 25458 bytes
-rw-r--r--doc/workflow/img/file_finder_find_file.pngbin42658 -> 35114 bytes
-rw-r--r--doc/workflow/img/forking_workflow_choose_namespace.pngbin70405 -> 59114 bytes
-rw-r--r--doc/workflow/img/forking_workflow_fork_button.pngbin26438 -> 20750 bytes
-rw-r--r--doc/workflow/img/forking_workflow_path_taken_error.pngbin22380 -> 17978 bytes
-rw-r--r--doc/workflow/img/new_branch_from_issue.pngbin120622 -> 54607 bytes
-rw-r--r--doc/workflow/img/revert_changes_commit.pngbin360098 -> 233750 bytes
-rw-r--r--doc/workflow/img/revert_changes_commit_modal.pngbin327932 -> 205046 bytes
-rw-r--r--doc/workflow/img/revert_changes_mr.pngbin367881 -> 241051 bytes
-rw-r--r--doc/workflow/img/revert_changes_mr_modal.pngbin335010 -> 211022 bytes
-rw-r--r--doc/workflow/img/todo_list_item.pngbin0 -> 58912 bytes
-rw-r--r--doc/workflow/img/todos_add_todo_sidebar.pngbin0 -> 120265 bytes
-rw-r--r--doc/workflow/img/todos_icon.pngbin7394 -> 3843 bytes
-rw-r--r--doc/workflow/img/todos_index.pngbin184839 -> 152040 bytes
-rw-r--r--doc/workflow/img/todos_mark_done_sidebar.pngbin0 -> 121303 bytes
-rw-r--r--doc/workflow/img/web_editor_new_branch_dropdown.pngbin34233 -> 20436 bytes
-rw-r--r--doc/workflow/img/web_editor_new_branch_page.pngbin21618 -> 11245 bytes
-rw-r--r--doc/workflow/img/web_editor_new_directory_dialog.pngbin26145 -> 13339 bytes
-rw-r--r--doc/workflow/img/web_editor_new_directory_dropdown.pngbin33714 -> 20007 bytes
-rw-r--r--doc/workflow/img/web_editor_new_file_dropdown.pngbin34978 -> 20680 bytes
-rw-r--r--doc/workflow/img/web_editor_new_file_editor.pngbin128658 -> 66261 bytes
-rw-r--r--doc/workflow/img/web_editor_new_push_widget.pngbin11220 -> 7076 bytes
-rw-r--r--doc/workflow/img/web_editor_new_tag_dropdown.pngbin33753 -> 20080 bytes
-rw-r--r--doc/workflow/img/web_editor_new_tag_page.pngbin75536 -> 36610 bytes
-rw-r--r--doc/workflow/img/web_editor_start_new_merge_request.pngbin14352 -> 8596 bytes
-rw-r--r--doc/workflow/img/web_editor_upload_file_dialog.pngbin46219 -> 21502 bytes
-rw-r--r--doc/workflow/img/web_editor_upload_file_dropdown.pngbin34835 -> 20651 bytes
-rw-r--r--doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.pngbin16121 -> 15288 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.pngbin53276 -> 30266 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_login.pngbin44444 -> 20797 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.pngbin35415 -> 20526 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.pngbin62552 -> 34836 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.pngbin157856 -> 77208 bytes
-rw-r--r--doc/workflow/importing/gitlab_importer/importer.pngbin40778 -> 18366 bytes
-rw-r--r--doc/workflow/importing/gitlab_importer/new_project_page.pngbin72663 -> 33589 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_importer.pngbin28033 -> 22711 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin17225 -> 13668 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_github.md20
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md2
-rw-r--r--doc/workflow/labels.md17
-rw-r--r--doc/workflow/labels/label1.pngbin5846 -> 0 bytes
-rw-r--r--doc/workflow/labels/label2.pngbin16931 -> 0 bytes
-rw-r--r--doc/workflow/labels/label3.pngbin19360 -> 0 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md4
-rw-r--r--doc/workflow/merge_commits.pngbin41422 -> 22181 bytes
-rw-r--r--doc/workflow/merge_request.pngbin169503 -> 98070 bytes
-rw-r--r--doc/workflow/merge_requests.md11
-rw-r--r--doc/workflow/merge_requests/commit_compare.pngbin110376 -> 65010 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff.pngbin166226 -> 103239 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff_without_whitespace.pngbin121476 -> 71896 bytes
-rw-r--r--doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.pngbin0 -> 17552 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds/enable.pngbin151112 -> 68769 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds/status.pngbin180318 -> 82655 bytes
-rw-r--r--doc/workflow/messy_flow.pngbin33829 -> 19314 bytes
-rw-r--r--doc/workflow/milestones/form.pngbin88591 -> 84872 bytes
-rw-r--r--doc/workflow/milestones/group_form.pngbin77087 -> 74429 bytes
-rw-r--r--doc/workflow/mr_inline_comments.pngbin193311 -> 117313 bytes
-rw-r--r--doc/workflow/notifications.md10
-rw-r--r--doc/workflow/notifications/settings.pngbin114727 -> 59256 bytes
-rw-r--r--doc/workflow/production_branch.pngbin21716 -> 10946 bytes
-rw-r--r--doc/workflow/protected_branches.md28
-rw-r--r--doc/workflow/protected_branches/protected_branches1.pngbin170113 -> 195061 bytes
-rw-r--r--doc/workflow/protected_branches/protected_branches2.pngbin25851 -> 41179 bytes
-rw-r--r--doc/workflow/protected_branches/protected_branches3.pngbin0 -> 110160 bytes
-rw-r--r--doc/workflow/rebase.pngbin123041 -> 68976 bytes
-rw-r--r--doc/workflow/release_branches.pngbin44173 -> 22163 bytes
-rw-r--r--doc/workflow/releases/new_tag.pngbin154755 -> 87330 bytes
-rw-r--r--doc/workflow/releases/tags.pngbin165449 -> 93016 bytes
-rw-r--r--doc/workflow/remove_checkbox.pngbin22272 -> 12339 bytes
-rw-r--r--doc/workflow/share_with_group.pngbin53784 -> 50450 bytes
-rw-r--r--doc/workflow/shortcuts.pngbin90936 -> 108209 bytes
-rw-r--r--doc/workflow/todos.md59
-rw-r--r--doc/workflow/wip_merge_requests/blocked_accept_button.pngbin65231 -> 32720 bytes
-rw-r--r--doc/workflow/wip_merge_requests/mark_as_wip.pngbin41549 -> 21640 bytes
-rw-r--r--doc/workflow/wip_merge_requests/unmark_as_wip.pngbin32151 -> 16606 bytes
-rw-r--r--features/admin/active_tab.feature22
-rw-r--r--features/admin/groups.feature7
-rw-r--r--features/admin/projects.feature4
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/dashboard/group.feature44
-rw-r--r--features/dashboard/new_project.feature4
-rw-r--r--features/dashboard/todos.feature7
-rw-r--r--features/project/active_tab.feature67
-rw-r--r--features/project/builds/summary.feature1
-rw-r--r--features/project/issues/issues.feature9
-rw-r--r--features/project/merge_requests.feature21
-rw-r--r--features/project/project.feature9
-rw-r--r--features/project/shortcuts.feature8
-rw-r--r--features/search.feature8
-rw-r--r--features/steps/admin/active_tab.rb30
-rw-r--r--features/steps/admin/groups.rb9
-rw-r--r--features/steps/admin/projects.rb9
-rw-r--r--features/steps/admin/users.rb2
-rw-r--r--features/steps/dashboard/active_tab.rb6
-rw-r--r--features/steps/dashboard/dashboard.rb2
-rw-r--r--features/steps/dashboard/group.rb42
-rw-r--r--features/steps/dashboard/issues.rb2
-rw-r--r--features/steps/dashboard/merge_requests.rb2
-rw-r--r--features/steps/dashboard/new_project.rb17
-rw-r--r--features/steps/dashboard/shortcuts.rb3
-rw-r--r--features/steps/dashboard/todos.rb41
-rw-r--r--features/steps/explore/projects.rb3
-rw-r--r--features/steps/group/members.rb14
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/profile/active_tab.rb4
-rw-r--r--features/steps/profile/notifications.rb6
-rw-r--r--features/steps/profile/profile.rb8
-rw-r--r--features/steps/project/active_tab.rb44
-rw-r--r--features/steps/project/archived.rb1
-rw-r--r--features/steps/project/builds/artifacts.rb4
-rw-r--r--features/steps/project/builds/summary.rb4
-rw-r--r--features/steps/project/commits/commits.rb12
-rw-r--r--features/steps/project/fork.rb2
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/hooks.rb2
-rw-r--r--features/steps/project/issues/award_emoji.rb8
-rw-r--r--features/steps/project/issues/filter_labels.rb2
-rw-r--r--features/steps/project/issues/issues.rb23
-rw-r--r--features/steps/project/issues/labels.rb16
-rw-r--r--features/steps/project/labels.rb2
-rw-r--r--features/steps/project/merge_requests.rb27
-rw-r--r--features/steps/project/network_graph.rb20
-rw-r--r--features/steps/project/project.rb18
-rw-r--r--features/steps/project/project_find_file.rb5
-rw-r--r--features/steps/project/project_milestone.rb2
-rw-r--r--features/steps/project/project_shortcuts.rb1
-rw-r--r--features/steps/project/snippets.rb4
-rw-r--r--features/steps/project/source/browse_files.rb36
-rw-r--r--features/steps/project/team_management.rb28
-rw-r--r--features/steps/project/wiki.rb4
-rw-r--r--features/steps/shared/active_tab.rb32
-rw-r--r--features/steps/shared/builds.rb6
-rw-r--r--features/steps/shared/diff_note.rb12
-rw-r--r--features/steps/shared/issuable.rb25
-rw-r--r--features/steps/shared/note.rb2
-rw-r--r--features/steps/shared/project.rb9
-rw-r--r--features/steps/shared/project_tab.rb20
-rw-r--r--features/steps/shared/shortcuts.rb2
-rw-r--r--features/steps/shared/sidebar_active_tab.rb35
-rw-r--r--features/steps/snippet_search.rb1
-rw-r--r--features/steps/snippets/snippets.rb4
-rw-r--r--features/steps/user.rb2
-rw-r--r--features/support/env.rb10
-rw-r--r--fixtures/emojis/digests.json4078
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb35
-rw-r--r--generator_templates/active_record/migration/migration.rb55
-rw-r--r--lib/api/api.rb47
-rw-r--r--lib/api/award_emoji.rb115
-rw-r--r--lib/api/branches.rb10
-rw-r--r--lib/api/builds.rb29
-rw-r--r--lib/api/commit_statuses.rb12
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/entities.rb97
-rw-r--r--lib/api/group_members.rb2
-rw-r--r--lib/api/groups.rb3
-rw-r--r--lib/api/helpers.rb43
-rw-r--r--lib/api/internal.rb43
-rw-r--r--lib/api/issues.rb78
-rw-r--r--lib/api/labels.rb6
-rw-r--r--lib/api/license_templates.rb (renamed from lib/api/licenses.rb)18
-rw-r--r--lib/api/merge_requests.rb52
-rw-r--r--lib/api/milestones.rb1
-rw-r--r--lib/api/notes.rb47
-rw-r--r--lib/api/project_hooks.rb1
-rw-r--r--lib/api/project_members.rb3
-rw-r--r--lib/api/projects.rb9
-rw-r--r--lib/api/repositories.rb10
-rw-r--r--lib/api/runners.rb12
-rw-r--r--lib/api/services.rb1
-rw-r--r--lib/api/session.rb3
-rw-r--r--lib/api/sidekiq_metrics.rb90
-rw-r--r--lib/api/subscriptions.rb60
-rw-r--r--lib/api/tags.rb6
-rw-r--r--lib/api/templates.rb36
-rw-r--r--lib/api/todos.rb82
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/award_emoji.rb84
-rw-r--r--lib/backup/database.rb4
-rw-r--r--lib/backup/manager.rb66
-rw-r--r--lib/backup/registry.rb13
-rw-r--r--lib/backup/repository.rb52
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb78
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb20
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb20
-rw-r--r--lib/banzai/filter/emoji_filter.rb4
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb14
-rw-r--r--lib/banzai/filter/external_link_filter.rb14
-rw-r--r--lib/banzai/filter/image_link_filter.rb9
-rw-r--r--lib/banzai/filter/inline_diff_filter.rb26
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb65
-rw-r--r--lib/banzai/filter/label_reference_filter.rb10
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb46
-rw-r--r--lib/banzai/filter/redactor_filter.rb24
-rw-r--r--lib/banzai/filter/reference_filter.rb31
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb65
-rw-r--r--lib/banzai/filter/relative_link_filter.rb51
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb9
-rw-r--r--lib/banzai/filter/upload_link_filter.rb11
-rw-r--r--lib/banzai/filter/user_reference_filter.rb73
-rw-r--r--lib/banzai/filter/wiki_link_filter.rb30
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb40
-rw-r--r--lib/banzai/lazy_reference.rb25
-rw-r--r--lib/banzai/note_renderer.rb22
-rw-r--r--lib/banzai/object_renderer.rb85
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb17
-rw-r--r--lib/banzai/pipeline/full_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/relative_link_pipeline.rb (renamed from lib/banzai/pipeline/reference_extraction_pipeline.rb)4
-rw-r--r--lib/banzai/redactor.rb69
-rw-r--r--lib/banzai/reference_extractor.rb48
-rw-r--r--lib/banzai/reference_parser.rb14
-rw-r--r--lib/banzai/reference_parser/base_parser.rb204
-rw-r--r--lib/banzai/reference_parser/commit_parser.rb34
-rw-r--r--lib/banzai/reference_parser/commit_range_parser.rb38
-rw-r--r--lib/banzai/reference_parser/external_issue_parser.rb25
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb54
-rw-r--r--lib/banzai/reference_parser/label_parser.rb11
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb11
-rw-r--r--lib/banzai/reference_parser/milestone_parser.rb11
-rw-r--r--lib/banzai/reference_parser/snippet_parser.rb11
-rw-r--r--lib/banzai/reference_parser/user_parser.rb92
-rw-r--r--lib/banzai/renderer.rb8
-rw-r--r--lib/ci/ansi2html.rb87
-rw-r--r--lib/ci/api/builds.rb5
-rw-r--r--lib/ci/api/entities.rb5
-rw-r--r--lib/ci/api/runners.rb15
-rw-r--r--lib/ci/charts.rb6
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb165
-rw-r--r--lib/container_registry/blob.rb48
-rw-r--r--lib/container_registry/client.rb68
-rw-r--r--lib/container_registry/config.rb16
-rw-r--r--lib/container_registry/registry.rb21
-rw-r--r--lib/container_registry/repository.rb48
-rw-r--r--lib/container_registry/tag.rb88
-rw-r--r--lib/disable_email_interceptor.rb1
-rw-r--r--lib/event_filter.rb2
-rw-r--r--lib/gitlab.rb7
-rw-r--r--lib/gitlab/access.rb2
-rw-r--r--lib/gitlab/asciidoc.rb1
-rw-r--r--lib/gitlab/auth.rb95
-rw-r--r--lib/gitlab/auth/ip_rate_limiter.rb42
-rw-r--r--lib/gitlab/award_emoji.rb93
-rw-r--r--lib/gitlab/backend/grack_auth.rb67
-rw-r--r--lib/gitlab/backend/shell.rb98
-rw-r--r--lib/gitlab/backend/shell_env.rb28
-rw-r--r--lib/gitlab/bitbucket_import/client.rb2
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb7
-rw-r--r--lib/gitlab/blame.rb3
-rw-r--r--lib/gitlab/build_data_builder.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/config.rb33
-rw-r--r--lib/gitlab/ci/config/loader.rb25
-rw-r--r--lib/gitlab/ci/config/node/boolean.rb18
-rw-r--r--lib/gitlab/ci/config/node/cache.rb27
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb67
-rw-r--r--lib/gitlab/ci/config/node/entry.rb92
-rw-r--r--lib/gitlab/ci/config/node/factory.rb48
-rw-r--r--lib/gitlab/ci/config/node/global.rb46
-rw-r--r--lib/gitlab/ci/config/node/image.rb18
-rw-r--r--lib/gitlab/ci/config/node/key.rb18
-rw-r--r--lib/gitlab/ci/config/node/legacy_validation_helpers.rb55
-rw-r--r--lib/gitlab/ci/config/node/paths.rb18
-rw-r--r--lib/gitlab/ci/config/node/script.rb18
-rw-r--r--lib/gitlab/ci/config/node/services.rb18
-rw-r--r--lib/gitlab/ci/config/node/stages.rb22
-rw-r--r--lib/gitlab/ci/config/node/undefined.rb30
-rw-r--r--lib/gitlab/ci/config/node/validatable.rb29
-rw-r--r--lib/gitlab/ci/config/node/validator.rb41
-rw-r--r--lib/gitlab/ci/config/node/validators.rb70
-rw-r--r--lib/gitlab/ci/config/node/variables.rb22
-rw-r--r--lib/gitlab/contributions_calendar.rb2
-rw-r--r--lib/gitlab/current_settings.rb31
-rw-r--r--lib/gitlab/database.rb27
-rw-r--r--lib/gitlab/database/migration_helpers.rb158
-rw-r--r--lib/gitlab/diff/diff_refs.rb36
-rw-r--r--lib/gitlab/diff/file.rb101
-rw-r--r--lib/gitlab/diff/highlight.rb31
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb36
-rw-r--r--lib/gitlab/diff/line.rb16
-rw-r--r--lib/gitlab/diff/line_mapper.rb64
-rw-r--r--lib/gitlab/diff/parallel_diff.rb32
-rw-r--r--lib/gitlab/diff/parser.rb15
-rw-r--r--lib/gitlab/diff/position.rb155
-rw-r--r--lib/gitlab/diff/position_tracer.rb168
-rw-r--r--lib/gitlab/email/message/repository_push.rb17
-rw-r--r--lib/gitlab/email/receiver.rb10
-rw-r--r--lib/gitlab/email/reply_parser.rb2
-rw-r--r--lib/gitlab/emoji.rb21
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb9
-rw-r--r--lib/gitlab/git/hook.rb21
-rw-r--r--lib/gitlab/git_access.rb11
-rw-r--r--lib/gitlab/github_import/base_formatter.rb4
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb33
-rw-r--r--lib/gitlab/github_import/client.rb80
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb5
-rw-r--r--lib/gitlab/github_import/hook_formatter.rb23
-rw-r--r--lib/gitlab/github_import/importer.rb146
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb4
-rw-r--r--lib/gitlab/github_import/label_formatter.rb4
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb4
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb58
-rw-r--r--lib/gitlab/gitlab_import/importer.rb8
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb4
-rw-r--r--lib/gitlab/gl_id.rb11
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb9
-rw-r--r--lib/gitlab/graphs/commits.rb2
-rw-r--r--lib/gitlab/highlight.rb27
-rw-r--r--lib/gitlab/import_export.rb39
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb47
-rw-r--r--lib/gitlab/import_export/command_line_util.rb41
-rw-r--r--lib/gitlab/import_export/error.rb5
-rw-r--r--lib/gitlab/import_export/file_importer.rb34
-rw-r--r--lib/gitlab/import_export/import_export.yml59
-rw-r--r--lib/gitlab/import_export/importer.rb72
-rw-r--r--lib/gitlab/import_export/members_mapper.rb67
-rw-r--r--lib/gitlab/import_export/project_creator.rb23
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb104
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb29
-rw-r--r--lib/gitlab/import_export/reader.rb115
-rw-r--r--lib/gitlab/import_export/relation_factory.rb130
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb39
-rw-r--r--lib/gitlab/import_export/repo_saver.rb35
-rw-r--r--lib/gitlab/import_export/saver.rb43
-rw-r--r--lib/gitlab/import_export/shared.rb29
-rw-r--r--lib/gitlab/import_export/uploads_restorer.rb14
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb35
-rw-r--r--lib/gitlab/import_export/version_checker.rb35
-rw-r--r--lib/gitlab/import_export/version_saver.rb24
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb33
-rw-r--r--lib/gitlab/import_sources.rb5
-rw-r--r--lib/gitlab/key_fingerprint.rb8
-rw-r--r--lib/gitlab/lazy.rb34
-rw-r--r--lib/gitlab/ldap/config.rb1
-rw-r--r--lib/gitlab/lfs/response.rb8
-rw-r--r--lib/gitlab/lfs/router.rb7
-rw-r--r--lib/gitlab/markup_helper.rb2
-rw-r--r--lib/gitlab/metrics/instrumentation.rb28
-rw-r--r--lib/gitlab/metrics/method_call.rb52
-rw-r--r--lib/gitlab/metrics/metric.rb21
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb31
-rw-r--r--lib/gitlab/metrics/sampler.rb6
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb34
-rw-r--r--lib/gitlab/metrics/system.rb20
-rw-r--r--lib/gitlab/metrics/transaction.rb41
-rw-r--r--lib/gitlab/middleware/go.rb2
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb24
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/o_auth/user.rb21
-rw-r--r--lib/gitlab/other_markup.rb1
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/protocol_access.rb13
-rw-r--r--lib/gitlab/redis.rb8
-rw-r--r--lib/gitlab/reference_extractor.rb19
-rw-r--r--lib/gitlab/regex.rb18
-rw-r--r--lib/gitlab/saml/auth_hash.rb2
-rw-r--r--lib/gitlab/saml/config.rb2
-rw-r--r--lib/gitlab/saml/user.rb5
-rw-r--r--lib/gitlab/sanitizers/svg.rb52
-rw-r--r--lib/gitlab/sanitizers/svg/whitelist.rb170
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb8
-rw-r--r--lib/gitlab/template/base_template.rb67
-rw-r--r--lib/gitlab/template/gitignore.rb22
-rw-r--r--lib/gitlab/template/gitlab_ci_yml.rb27
-rw-r--r--lib/gitlab/timeless.rb16
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/url_sanitizer.rb (renamed from lib/gitlab/import_url.rb)25
-rw-r--r--lib/gitlab/visibility_level.rb7
-rw-r--r--lib/gitlab/workhorse.rb45
-rw-r--r--lib/json_web_token/rsa_token.rb42
-rw-r--r--lib/json_web_token/token.rb46
-rw-r--r--lib/support/nginx/registry-ssl53
-rw-r--r--lib/tasks/gemojione.rake2
-rw-r--r--lib/tasks/gitlab/backup.rake98
-rw-r--r--lib/tasks/gitlab/check.rake319
-rw-r--r--lib/tasks/gitlab/cleanup.rake81
-rw-r--r--lib/tasks/gitlab/db.rake20
-rw-r--r--lib/tasks/gitlab/git.rake8
-rw-r--r--lib/tasks/gitlab/import.rake98
-rw-r--r--lib/tasks/gitlab/import_export.rake13
-rw-r--r--lib/tasks/gitlab/info.rake31
-rw-r--r--lib/tasks/gitlab/list_repos.rake2
-rw-r--r--lib/tasks/gitlab/setup.rake2
-rw-r--r--lib/tasks/gitlab/shell.rake16
-rw-r--r--lib/tasks/gitlab/task_helpers.rake22
-rw-r--r--lib/tasks/gitlab/two_factor.rake8
-rw-r--r--lib/tasks/gitlab/update_commit_count.rake6
-rw-r--r--lib/tasks/gitlab/update_templates.rake54
-rw-r--r--lib/tasks/gitlab/web_hook.rake6
-rw-r--r--lib/tasks/migrate/migrate_iids.rake6
-rw-r--r--lib/tasks/rubocop.rake1
-rw-r--r--lib/tasks/spinach.rake2
-rw-r--r--lib/tasks/test.rake4
-rw-r--r--lib/uploaded_file.rb1
-rw-r--r--public/apple-touch-icon-precomposed.pngbin11097 -> 9509 bytes
-rw-r--r--public/apple-touch-icon.pngbin11097 -> 9509 bytes
-rw-r--r--public/robots.txt1
-rw-r--r--rubocop/cop/migration/add_index.rb46
-rw-r--r--rubocop/cop/migration/column_with_default.rb50
-rw-r--r--rubocop/migration_helpers.rb10
-rw-r--r--rubocop/rubocop.rb3
-rwxr-xr-xscripts/merge-reports29
-rwxr-xr-xscripts/prepare_build.sh14
-rw-r--r--shared/registry/.gitkeep0
-rw-r--r--spec/config/mail_room_spec.rb2
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb6
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb6
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb6
-rw-r--r--spec/controllers/admin/users_controller_spec.rb78
-rw-r--r--spec/controllers/application_controller_spec.rb70
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb10
-rw-r--r--spec/controllers/blob_controller_spec.rb5
-rw-r--r--spec/controllers/commit_controller_spec.rb4
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb198
-rw-r--r--spec/controllers/groups/notification_settings_controller_spec.rb32
-rw-r--r--spec/controllers/groups_controller_spec.rb12
-rw-r--r--spec/controllers/health_check_controller_spec.rb105
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb1
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb1
-rw-r--r--spec/controllers/import/github_controller_spec.rb44
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb1
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb1
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb1
-rw-r--r--spec/controllers/invites_controller_spec.rb4
-rw-r--r--spec/controllers/namespaces_controller_spec.rb4
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb166
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb29
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb25
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb14
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb40
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb16
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb12
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb4
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb47
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb52
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb133
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb36
-rw-r--r--spec/controllers/projects/notification_settings_controller_spec.rb38
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb249
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb12
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb8
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb16
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb102
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb3
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb24
-rw-r--r--spec/controllers/projects_controller_spec.rb78
-rw-r--r--spec/controllers/registrations_controller_spec.rb32
-rw-r--r--spec/controllers/sessions_controller_spec.rb55
-rw-r--r--spec/controllers/snippets_controller_spec.rb28
-rw-r--r--spec/controllers/uploads_controller_spec.rb28
-rw-r--r--spec/controllers/users_controller_spec.rb29
-rw-r--r--spec/factories/award_emoji.rb12
-rw-r--r--spec/factories/ci/builds.rb4
-rw-r--r--spec/factories/ci/pipelines.rb (renamed from spec/factories/ci/commits.rb)10
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/deployments.rb13
-rw-r--r--spec/factories/environments.rb7
-rw-r--r--spec/factories/notes.rb57
-rw-r--r--spec/factories/notification_settings.rb8
-rw-r--r--spec/factories/personal_access_tokens.rb9
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/factories/todos.rb8
-rw-r--r--spec/factories/u2f_registrations.rb8
-rw-r--r--spec/factories/users.rb14
-rw-r--r--spec/factories/wiki_pages.rb2
-rw-r--r--spec/factories_spec.rb4
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb30
-rw-r--r--spec/features/admin/admin_builds_spec.rb27
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb66
-rw-r--r--spec/features/admin/admin_health_check_spec.rb55
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_runners_spec.rb66
-rw-r--r--spec/features/admin/admin_system_info_spec.rb17
-rw-r--r--spec/features/admin/admin_users_spec.rb23
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb51
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/builds_spec.rb207
-rw-r--r--spec/features/commits_spec.rb66
-rw-r--r--spec/features/container_registry_spec.rb45
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb46
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb1
-rw-r--r--spec/features/environments_spec.rb160
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/groups/members/last_owner_cannot_leave_group_spec.rb16
-rw-r--r--spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb16
-rw-r--r--spec/features/groups/members/member_leaves_group_spec.rb21
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb47
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb64
-rw-r--r--spec/features/issues/award_emoji_spec.rb2
-rw-r--r--spec/features/issues/award_spec.rb49
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb359
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb50
-rw-r--r--spec/features/issues/filter_issues_spec.rb171
-rw-r--r--spec/features/issues/move_spec.rb32
-rw-r--r--spec/features/issues/note_polling_spec.rb5
-rw-r--r--spec/features/issues/todo_spec.rb43
-rw-r--r--spec/features/issues/update_issues_spec.rb23
-rw-r--r--spec/features/issues_spec.rb156
-rw-r--r--spec/features/login_spec.rb36
-rw-r--r--spec/features/markdown_spec.rb21
-rw-r--r--spec/features/merge_requests/award_spec.rb49
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb58
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb8
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb105
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb24
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb39
-rw-r--r--spec/features/participants_autocomplete_spec.rb6
-rw-r--r--spec/features/pipelines_spec.rb189
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb94
-rw-r--r--spec/features/profiles/preferences_spec.rb8
-rw-r--r--spec/features/projects/badges/list_spec.rb13
-rw-r--r--spec/features/projects/commit/builds_spec.rb7
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb31
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb31
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb22
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb16
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb66
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin0 -> 687442 bytes
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb84
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb116
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb17
-rw-r--r--spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb47
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb21
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb47
-rw-r--r--spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb16
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb19
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb16
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb16
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb55
-rw-r--r--spec/features/projects/shortcuts_spec.rb (renamed from spec/features/project/shortcuts_spec.rb)4
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/protected_branches_spec.rb84
-rw-r--r--spec/features/runners_spec.rb37
-rw-r--r--spec/features/search_spec.rb76
-rw-r--r--spec/features/security/group/internal_access_spec.rb1
-rw-r--r--spec/features/security/group/private_access_spec.rb1
-rw-r--r--spec/features/security/group/public_access_spec.rb1
-rw-r--r--spec/features/security/project/internal_access_spec.rb138
-rw-r--r--spec/features/security/project/private_access_spec.rb102
-rw-r--r--spec/features/security/project/public_access_spec.rb76
-rw-r--r--spec/features/signup_spec.rb43
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb1
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb5
-rw-r--r--spec/features/todos/target_state_spec.rb65
-rw-r--r--spec/features/todos/todos_spec.rb40
-rw-r--r--spec/features/u2f_spec.rb228
-rw-r--r--spec/features/users_spec.rb1
-rw-r--r--spec/features/variables_spec.rb61
-rw-r--r--spec/finders/group_projects_finder_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb180
-rw-r--r--spec/finders/notes_finder_spec.rb23
-rw-r--r--spec/finders/snippets_finder_spec.rb1
-rw-r--r--spec/fixtures/container_registry/config_blob.json1
-rw-r--r--spec/fixtures/container_registry/tag_manifest.json16
-rw-r--r--spec/fixtures/container_registry/tag_manifest_1.json32
-rw-r--r--spec/fixtures/dk.pngbin1143 -> 1062 bytes
-rw-r--r--spec/fixtures/markdown.md.erb25
-rw-r--r--spec/fixtures/parallel_diff_result.yml504
-rw-r--r--spec/helpers/application_helper_spec.rb45
-rw-r--r--spec/helpers/auth_helper_spec.rb47
-rw-r--r--spec/helpers/ci_status_helper_spec.rb4
-rw-r--r--spec/helpers/diff_helper_spec.rb6
-rw-r--r--spec/helpers/events_helper_spec.rb95
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb3
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb79
-rw-r--r--spec/helpers/issues_helper_spec.rb27
-rw-r--r--spec/helpers/members_helper_spec.rb104
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb10
-rw-r--r--spec/helpers/notes_helper_spec.rb46
-rw-r--r--spec/helpers/projects_helper_spec.rb20
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb7
-rw-r--r--spec/initializers/6_validations_spec.rb41
-rw-r--r--spec/initializers/settings_spec.rb3
-rw-r--r--spec/initializers/trusted_proxies_spec.rb12
-rw-r--r--spec/javascripts/application_spec.js.coffee30
-rw-r--r--spec/javascripts/awards_handler_spec.js.coffee200
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js.coffee22
-rw-r--r--spec/javascripts/fixtures/application.html.haml2
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml52
-rw-r--r--spec/javascripts/fixtures/behaviors/quick_submit.html.haml2
-rw-r--r--spec/javascripts/fixtures/emoji_menu.coffee957
-rw-r--r--spec/javascripts/fixtures/right_sidebar.html.haml13
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml10
-rw-r--r--spec/javascripts/fixtures/u2f/authenticate.html.haml1
-rw-r--r--spec/javascripts/fixtures/u2f/register.html.haml2
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js (renamed from spec/javascripts/stat_graph_contributors_graph_spec.js)2
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js (renamed from spec/javascripts/stat_graph_contributors_util_spec.js)14
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js (renamed from spec/javascripts/stat_graph_spec.js)2
-rw-r--r--spec/javascripts/issue_spec.js.coffee13
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee2
-rw-r--r--spec/javascripts/new_branch_spec.js.coffee2
-rw-r--r--spec/javascripts/notes_spec.js.coffee2
-rw-r--r--spec/javascripts/project_title_spec.js.coffee3
-rw-r--r--spec/javascripts/right_sidebar_spec.js.coffee69
-rw-r--r--spec/javascripts/search_autocomplete_spec.js.coffee149
-rw-r--r--spec/javascripts/u2f/authenticate_spec.coffee52
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js.coffee15
-rw-r--r--spec/javascripts/u2f/register_spec.js.coffee57
-rw-r--r--spec/lib/award_emoji_spec.rb26
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb52
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/inline_diff_filter_spec.rb68
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb81
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb60
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb21
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb145
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb50
-rw-r--r--spec/lib/banzai/filter/reference_filter_spec.rb45
-rw-r--r--spec/lib/banzai/filter/reference_gatherer_filter_spec.rb87
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb20
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb50
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb26
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb25
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb120
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb107
-rw-r--r--spec/lib/banzai/redactor_spec.rb53
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb237
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb113
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb120
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb62
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb79
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb30
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb189
-rw-r--r--spec/lib/ci/ansi2html_spec.rb120
-rw-r--r--spec/lib/ci/charts_spec.rb14
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb296
-rw-r--r--spec/lib/container_registry/blob_spec.rb61
-rw-r--r--spec/lib/container_registry/registry_spec.rb28
-rw-r--r--spec/lib/container_registry/repository_spec.rb65
-rw-r--r--spec/lib/container_registry/tag_spec.rb128
-rw-r--r--spec/lib/disable_email_interceptor_spec.rb4
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb3
-rw-r--r--spec/lib/gitlab/auth_spec.rb56
-rw-r--r--spec/lib/gitlab/award_emoji_spec.rb41
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb209
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb28
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb26
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/config/loader_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/config/node/boolean_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/cache_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb228
-rw-r--r--spec/lib/gitlab/ci/config/node/image_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/node/key_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/paths_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config/node/services_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/node/stages_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/node/validatable_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/config/node/validator_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/config/node/variables_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb79
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb36
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb160
-rw-r--r--spec/lib/gitlab/database_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb1
-rw-r--r--spec/lib/gitlab/diff/line_mapper_spec.rb137
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb3
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb341
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb1758
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb4
-rw-r--r--spec/lib/gitlab/fogbugz_import/client_spec.rb1
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb4
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb8
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb70
-rw-r--r--spec/lib/gitlab/git_access_spec.rb38
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb78
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/hook_formatter_spec.rb65
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb1
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb48
-rw-r--r--spec/lib/gitlab/gitlab_import/client_spec.rb4
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/graphs/commits_spec.rb39
-rw-r--r--spec/lib/gitlab/highlight_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb51
-rw-r--r--spec/lib/gitlab/import_export/project.json7382
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb150
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb86
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb24
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb27
-rw-r--r--spec/lib/gitlab/import_url_spec.rb21
-rw-r--r--spec/lib/gitlab/lazy_spec.rb37
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb1
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb569
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb80
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb91
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb45
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb17
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb119
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb16
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb31
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb73
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb24
-rw-r--r--spec/lib/gitlab/popen_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb37
-rw-r--r--spec/lib/gitlab/sanitizers/svg_spec.rb94
-rw-r--r--spec/lib/gitlab/search_results_spec.rb16
-rw-r--r--spec/lib/gitlab/sherlock/collection_spec.rb6
-rw-r--r--spec/lib/gitlab/sherlock/query_spec.rb2
-rw-r--r--spec/lib/gitlab/sherlock/transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/template/gitignore_spec.rb40
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb8
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb67
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/lib/gitlab_spec.rb6
-rw-r--r--spec/lib/json_web_token/rsa_token_spec.rb43
-rw-r--r--spec/lib/json_web_token/token_spec.rb18
-rw-r--r--spec/mailers/notify_spec.rb356
-rw-r--r--spec/mailers/previews/devise_mailer_preview.rb30
-rw-r--r--spec/mailers/shared/notify.rb4
-rw-r--r--spec/models/ability_spec.rb117
-rw-r--r--spec/models/application_setting_spec.rb19
-rw-r--r--spec/models/award_emoji_spec.rb30
-rw-r--r--spec/models/build_spec.rb276
-rw-r--r--spec/models/ci/commit_spec.rb404
-rw-r--r--spec/models/ci/pipeline_spec.rb416
-rw-r--r--spec/models/ci/runner_project_spec.rb5
-rw-r--r--spec/models/ci/runner_spec.rb273
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_range_spec.rb34
-rw-r--r--spec/models/commit_spec.rb52
-rw-r--r--spec/models/commit_status_spec.rb65
-rw-r--r--spec/models/concerns/access_requestable_spec.rb40
-rw-r--r--spec/models/concerns/awardable_spec.rb48
-rw-r--r--spec/models/concerns/issuable_spec.rb97
-rw-r--r--spec/models/concerns/mentionable_spec.rb37
-rw-r--r--spec/models/concerns/milestoneish_spec.rb14
-rw-r--r--spec/models/concerns/participable_spec.rb93
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb1
-rw-r--r--spec/models/concerns/subscribable_spec.rb10
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb6
-rw-r--r--spec/models/deployment_spec.rb17
-rw-r--r--spec/models/diff_note_spec.rb191
-rw-r--r--spec/models/email_spec.rb2
-rw-r--r--spec/models/environment_spec.rb14
-rw-r--r--spec/models/event_spec.rb38
-rw-r--r--spec/models/forked_project_link_spec.rb3
-rw-r--r--spec/models/generic_commit_status_spec.rb8
-rw-r--r--spec/models/group_spec.rb89
-rw-r--r--spec/models/hooks/service_hook_spec.rb4
-rw-r--r--spec/models/hooks/system_hook_spec.rb20
-rw-r--r--spec/models/hooks/web_hook_spec.rb4
-rw-r--r--spec/models/identity_spec.rb1
-rw-r--r--spec/models/issue_spec.rb57
-rw-r--r--spec/models/jira_issue_spec.rb30
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/label_spec.rb13
-rw-r--r--spec/models/legacy_diff_note_spec.rb76
-rw-r--r--spec/models/member_spec.rb96
-rw-r--r--spec/models/members/group_member_spec.rb18
-rw-r--r--spec/models/members/project_member_spec.rb61
-rw-r--r--spec/models/merge_request_spec.rb300
-rw-r--r--spec/models/milestone_spec.rb33
-rw-r--r--spec/models/namespace_spec.rb25
-rw-r--r--spec/models/note_spec.rb196
-rw-r--r--spec/models/notification_setting_spec.rb43
-rw-r--r--spec/models/personal_access_token_spec.rb15
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb14
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb49
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb151
-rw-r--r--spec/models/project_services/jira_service_spec.rb8
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb23
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb11
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb4
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb1
-rw-r--r--spec/models/project_services/slack_service_spec.rb68
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb12
-rw-r--r--spec/models/project_spec.rb420
-rw-r--r--spec/models/project_team_spec.rb144
-rw-r--r--spec/models/project_wiki_spec.rb16
-rw-r--r--spec/models/protected_branch_spec.rb125
-rw-r--r--spec/models/repository_spec.rb132
-rw-r--r--spec/models/service_spec.rb35
-rw-r--r--spec/models/snippet_spec.rb69
-rw-r--r--spec/models/user_spec.rb208
-rw-r--r--spec/requests/api/api_helpers_spec.rb75
-rw-r--r--spec/requests/api/award_emoji_spec.rb197
-rw-r--r--spec/requests/api/branches_spec.rb42
-rw-r--r--spec/requests/api/builds_spec.rb148
-rw-r--r--spec/requests/api/commit_statuses_spec.rb37
-rw-r--r--spec/requests/api/commits_spec.rb42
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb7
-rw-r--r--spec/requests/api/files_spec.rb24
-rw-r--r--spec/requests/api/fork_spec.rb12
-rw-r--r--spec/requests/api/group_members_spec.rb46
-rw-r--r--spec/requests/api/groups_spec.rb114
-rw-r--r--spec/requests/api/internal_spec.rb107
-rw-r--r--spec/requests/api/issues_spec.rb344
-rw-r--r--spec/requests/api/keys_spec.rb6
-rw-r--r--spec/requests/api/labels_spec.rb133
-rw-r--r--spec/requests/api/license_templates_spec.rb (renamed from spec/requests/api/licenses_spec.rb)20
-rw-r--r--spec/requests/api/merge_requests_spec.rb184
-rw-r--r--spec/requests/api/milestones_spec.rb47
-rw-r--r--spec/requests/api/namespaces_spec.rb10
-rw-r--r--spec/requests/api/notes_spec.rb146
-rw-r--r--spec/requests/api/project_hooks_spec.rb36
-rw-r--r--spec/requests/api/project_members_spec.rb38
-rw-r--r--spec/requests/api/project_snippets_spec.rb12
-rw-r--r--spec/requests/api/projects_spec.rb251
-rw-r--r--spec/requests/api/repositories_spec.rb40
-rw-r--r--spec/requests/api/runners_spec.rb129
-rw-r--r--spec/requests/api/services_spec.rb13
-rw-r--r--spec/requests/api/session_spec.rb8
-rw-r--r--spec/requests/api/settings_spec.rb14
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb40
-rw-r--r--spec/requests/api/system_hooks_spec.rb16
-rw-r--r--spec/requests/api/tags_spec.rb40
-rw-r--r--spec/requests/api/templates_spec.rb52
-rw-r--r--spec/requests/api/todos_spec.rb190
-rw-r--r--spec/requests/api/triggers_spec.rb56
-rw-r--r--spec/requests/api/users_spec.rb205
-rw-r--r--spec/requests/api/variables_spec.rb38
-rw-r--r--spec/requests/ci/api/builds_spec.rb228
-rw-r--r--spec/requests/ci/api/runners_spec.rb83
-rw-r--r--spec/requests/ci/api/triggers_spec.rb28
-rw-r--r--spec/requests/git_http_spec.rb398
-rw-r--r--spec/requests/jwt_controller_spec.rb72
-rw-r--r--spec/routing/admin_routing_spec.rb8
-rw-r--r--spec/routing/project_routing_spec.rb1
-rw-r--r--spec/routing/routing_spec.rb52
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb242
-rw-r--r--spec/services/ci/create_builds_service_spec.rb8
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb6
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb4
-rw-r--r--spec/services/ci/register_build_service_spec.rb66
-rw-r--r--spec/services/create_commit_builds_service_spec.rb233
-rw-r--r--spec/services/create_deployment_service_spec.rb119
-rw-r--r--spec/services/create_tag_service_spec.rb4
-rw-r--r--spec/services/destroy_group_service_spec.rb8
-rw-r--r--spec/services/git_hooks_service_spec.rb12
-rw-r--r--spec/services/git_push_service_spec.rb90
-rw-r--r--spec/services/git_tag_push_service_spec.rb25
-rw-r--r--spec/services/groups/create_service_spec.rb4
-rw-r--r--spec/services/issues/bulk_update_service_spec.rb286
-rw-r--r--spec/services/issues/create_service_spec.rb6
-rw-r--r--spec/services/issues/move_service_spec.rb11
-rw-r--r--spec/services/issues/update_service_spec.rb70
-rw-r--r--spec/services/members/destroy_service_spec.rb71
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb81
-rw-r--r--spec/services/merge_requests/create_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb25
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb48
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb29
-rw-r--r--spec/services/merge_requests/update_service_spec.rb8
-rw-r--r--spec/services/notes/create_service_spec.rb30
-rw-r--r--spec/services/notes/diff_position_update_service_spec.rb175
-rw-r--r--spec/services/notification_service_spec.rb444
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb12
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb31
-rw-r--r--spec/services/projects/fork_service_spec.rb27
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb2
-rw-r--r--spec/services/projects/import_service_spec.rb16
-rw-r--r--spec/services/projects/transfer_service_spec.rb12
-rw-r--r--spec/services/search/snippet_service_spec.rb59
-rw-r--r--spec/services/system_note_service_spec.rb29
-rw-r--r--spec/services/todo_service_spec.rb170
-rw-r--r--spec/spec_helper.rb10
-rw-r--r--spec/support/fake_u2f_device.rb36
-rw-r--r--spec/support/filter_spec_helper.rb3
-rw-r--r--spec/support/import_export/import_export.yml20
-rw-r--r--spec/support/import_spec_helper.rb (renamed from spec/controllers/import/import_spec_helper.rb)2
-rw-r--r--spec/support/jira_service_helper.rb11
-rw-r--r--spec/support/login_helpers.rb9
-rw-r--r--spec/support/markdown_feature.rb10
-rw-r--r--spec/support/matchers/markdown_matchers.rb12
-rw-r--r--spec/support/reference_parser_helpers.rb5
-rw-r--r--spec/support/stub_gitlab_calls.rb45
-rw-r--r--spec/support/test_env.rb12
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb143
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb62
-rw-r--r--spec/teaspoon_env.rb50
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb37
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb65
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb69
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb18
-rw-r--r--spec/workers/project_cache_worker_spec.rb1
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb29
-rw-r--r--spec/workers/repository_fork_worker_spec.rb9
-rw-r--r--spec/workers/repository_import_worker_spec.rb26
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb19
-rwxr-xr-xvendor/assets/javascripts/jquery.scrollTo.js210
-rw-r--r--vendor/assets/javascripts/raphael.js8239
-rw-r--r--vendor/assets/javascripts/task_list.js.coffee258
-rw-r--r--vendor/assets/javascripts/u2f.js748
-rw-r--r--vendor/assets/stylesheets/animate.css11
-rw-r--r--vendor/gitignore/Actionscript.gitignore19
-rw-r--r--vendor/gitignore/Ada.gitignore5
-rw-r--r--vendor/gitignore/Agda.gitignore1
-rw-r--r--vendor/gitignore/Android.gitignore41
-rw-r--r--vendor/gitignore/AppEngine.gitignore2
-rw-r--r--vendor/gitignore/AppceleratorTitanium.gitignore3
-rw-r--r--vendor/gitignore/ArchLinuxPackages.gitignore13
-rw-r--r--vendor/gitignore/Autotools.gitignore18
-rw-r--r--vendor/gitignore/C++.gitignore32
-rw-r--r--vendor/gitignore/C.gitignore36
-rw-r--r--vendor/gitignore/CFWheels.gitignore12
-rw-r--r--vendor/gitignore/CMake.gitignore7
-rw-r--r--vendor/gitignore/CUDA.gitignore6
-rw-r--r--vendor/gitignore/CakePHP.gitignore25
-rw-r--r--vendor/gitignore/ChefCookbook.gitignore9
l---------vendor/gitignore/Clojure.gitignore1
-rw-r--r--vendor/gitignore/CodeIgniter.gitignore6
-rw-r--r--vendor/gitignore/CommonLisp.gitignore3
-rw-r--r--vendor/gitignore/Composer.gitignore6
-rw-r--r--vendor/gitignore/Concrete5.gitignore4
-rw-r--r--vendor/gitignore/Coq.gitignore3
-rw-r--r--vendor/gitignore/CraftCMS.gitignore3
-rw-r--r--vendor/gitignore/D.gitignore24
-rw-r--r--vendor/gitignore/DM.gitignore5
-rw-r--r--vendor/gitignore/Dart.gitignore27
-rw-r--r--vendor/gitignore/Delphi.gitignore66
-rw-r--r--vendor/gitignore/Drupal.gitignore36
-rw-r--r--vendor/gitignore/EPiServer.gitignore4
-rw-r--r--vendor/gitignore/Eagle.gitignore44
-rw-r--r--vendor/gitignore/Elisp.gitignore5
-rw-r--r--vendor/gitignore/Elixir.gitignore5
-rw-r--r--vendor/gitignore/Elm.gitignore4
-rw-r--r--vendor/gitignore/Erlang.gitignore10
-rw-r--r--vendor/gitignore/ExpressionEngine.gitignore19
-rw-r--r--vendor/gitignore/ExtJs.gitignore4
-rw-r--r--vendor/gitignore/Fancy.gitignore2
-rw-r--r--vendor/gitignore/Finale.gitignore13
-rw-r--r--vendor/gitignore/ForceDotCom.gitignore4
l---------vendor/gitignore/Fortran.gitignore1
-rw-r--r--vendor/gitignore/FuelPHP.gitignore21
-rw-r--r--vendor/gitignore/GWT.gitignore28
-rw-r--r--vendor/gitignore/Gcov.gitignore5
-rw-r--r--vendor/gitignore/GitBook.gitignore16
-rw-r--r--vendor/gitignore/Global/Anjuta.gitignore3
-rw-r--r--vendor/gitignore/Global/Archives.gitignore27
-rw-r--r--vendor/gitignore/Global/Bazaar.gitignore2
-rw-r--r--vendor/gitignore/Global/BricxCC.gitignore4
-rw-r--r--vendor/gitignore/Global/CVS.gitignore4
-rw-r--r--vendor/gitignore/Global/Calabash.gitignore10
-rw-r--r--vendor/gitignore/Global/Cloud9.gitignore3
-rw-r--r--vendor/gitignore/Global/CodeKit.gitignore3
-rw-r--r--vendor/gitignore/Global/DartEditor.gitignore2
-rw-r--r--vendor/gitignore/Global/Dreamweaver.gitignore7
-rw-r--r--vendor/gitignore/Global/Dropbox.gitignore4
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore51
-rw-r--r--vendor/gitignore/Global/EiffelStudio.gitignore2
-rw-r--r--vendor/gitignore/Global/Emacs.gitignore42
-rw-r--r--vendor/gitignore/Global/Ensime.gitignore4
-rw-r--r--vendor/gitignore/Global/Espresso.gitignore1
-rw-r--r--vendor/gitignore/Global/FlexBuilder.gitignore3
-rw-r--r--vendor/gitignore/Global/GPG.gitignore2
-rw-r--r--vendor/gitignore/Global/IPythonNotebook.gitignore2
-rw-r--r--vendor/gitignore/Global/JDeveloper.gitignore13
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore44
-rw-r--r--vendor/gitignore/Global/KDevelop4.gitignore2
-rw-r--r--vendor/gitignore/Global/Kate.gitignore3
-rw-r--r--vendor/gitignore/Global/Lazarus.gitignore30
-rw-r--r--vendor/gitignore/Global/LibreOffice.gitignore2
-rw-r--r--vendor/gitignore/Global/Linux.gitignore10
-rw-r--r--vendor/gitignore/Global/LyX.gitignore4
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore19
-rw-r--r--vendor/gitignore/Global/Mercurial.gitignore6
-rw-r--r--vendor/gitignore/Global/MicrosoftOffice.gitignore16
-rw-r--r--vendor/gitignore/Global/ModelSim.gitignore23
-rw-r--r--vendor/gitignore/Global/Momentics.gitignore8
-rw-r--r--vendor/gitignore/Global/MonoDevelop.gitignore8
-rw-r--r--vendor/gitignore/Global/NetBeans.gitignore7
-rw-r--r--vendor/gitignore/Global/Ninja.gitignore2
-rw-r--r--vendor/gitignore/Global/NotepadPP.gitignore2
-rw-r--r--vendor/gitignore/Global/OSX.gitignore25
-rw-r--r--vendor/gitignore/Global/Otto.gitignore1
-rw-r--r--vendor/gitignore/Global/README.md10
-rw-r--r--vendor/gitignore/Global/Redcar.gitignore1
-rw-r--r--vendor/gitignore/Global/Redis.gitignore3
-rw-r--r--vendor/gitignore/Global/SBT.gitignore9
-rw-r--r--vendor/gitignore/Global/SVN.gitignore1
-rw-r--r--vendor/gitignore/Global/SlickEdit.gitignore11
-rw-r--r--vendor/gitignore/Global/SublimeText.gitignore27
-rw-r--r--vendor/gitignore/Global/SynopsysVCS.gitignore36
-rw-r--r--vendor/gitignore/Global/Tags.gitignore16
-rw-r--r--vendor/gitignore/Global/TextMate.gitignore3
-rw-r--r--vendor/gitignore/Global/TortoiseGit.gitignore2
-rw-r--r--vendor/gitignore/Global/Vagrant.gitignore1
-rw-r--r--vendor/gitignore/Global/Vim.gitignore10
-rw-r--r--vendor/gitignore/Global/VirtualEnv.gitignore12
-rw-r--r--vendor/gitignore/Global/VisualStudioCode.gitignore2
-rw-r--r--vendor/gitignore/Global/WebMethods.gitignore14
-rw-r--r--vendor/gitignore/Global/Windows.gitignore18
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore23
-rw-r--r--vendor/gitignore/Global/XilinxISE.gitignore67
-rw-r--r--vendor/gitignore/Go.gitignore24
-rw-r--r--vendor/gitignore/Gradle.gitignore14
-rw-r--r--vendor/gitignore/Grails.gitignore33
-rw-r--r--vendor/gitignore/Haskell.gitignore19
-rw-r--r--vendor/gitignore/IGORPro.gitignore5
-rw-r--r--vendor/gitignore/Idris.gitignore2
-rw-r--r--vendor/gitignore/Java.gitignore12
-rw-r--r--vendor/gitignore/Jboss.gitignore19
-rw-r--r--vendor/gitignore/Jekyll.gitignore3
-rw-r--r--vendor/gitignore/Joomla.gitignore546
-rw-r--r--vendor/gitignore/Julia.gitignore4
-rw-r--r--vendor/gitignore/KiCad.gitignore20
-rw-r--r--vendor/gitignore/Kohana.gitignore2
-rw-r--r--vendor/gitignore/LICENSE121
-rw-r--r--vendor/gitignore/LabVIEW.gitignore16
-rw-r--r--vendor/gitignore/Laravel.gitignore15
-rw-r--r--vendor/gitignore/Leiningen.gitignore12
-rw-r--r--vendor/gitignore/LemonStand.gitignore21
-rw-r--r--vendor/gitignore/Lilypond.gitignore6
-rw-r--r--vendor/gitignore/Lithium.gitignore2
-rw-r--r--vendor/gitignore/Lua.gitignore41
-rw-r--r--vendor/gitignore/Magento.gitignore104
-rw-r--r--vendor/gitignore/Maven.gitignore9
-rw-r--r--vendor/gitignore/Mercury.gitignore13
-rw-r--r--vendor/gitignore/MetaProgrammingSystem.gitignore16
-rw-r--r--vendor/gitignore/Nanoc.gitignore10
-rw-r--r--vendor/gitignore/Nim.gitignore1
-rw-r--r--vendor/gitignore/Node.gitignore38
-rw-r--r--vendor/gitignore/OCaml.gitignore20
-rw-r--r--vendor/gitignore/Objective-C.gitignore60
-rw-r--r--vendor/gitignore/Opa.gitignore13
-rw-r--r--vendor/gitignore/OpenCart.gitignore13
-rw-r--r--vendor/gitignore/OracleForms.gitignore8
-rw-r--r--vendor/gitignore/Packer.gitignore5
-rw-r--r--vendor/gitignore/Perl.gitignore20
-rw-r--r--vendor/gitignore/Phalcon.gitignore2
-rw-r--r--vendor/gitignore/PlayFramework.gitignore15
-rw-r--r--vendor/gitignore/Plone.gitignore18
-rw-r--r--vendor/gitignore/Prestashop.gitignore32
-rw-r--r--vendor/gitignore/Processing.gitignore7
-rw-r--r--vendor/gitignore/Python.gitignore89
-rw-r--r--vendor/gitignore/Qooxdoo.gitignore5
-rw-r--r--vendor/gitignore/Qt.gitignore38
-rw-r--r--vendor/gitignore/R.gitignore33
-rw-r--r--vendor/gitignore/ROS.gitignore47
-rw-r--r--vendor/gitignore/Rails.gitignore42
-rw-r--r--vendor/gitignore/RhodesRhomobile.gitignore9
-rw-r--r--vendor/gitignore/Ruby.gitignore50
-rw-r--r--vendor/gitignore/Rust.gitignore7
-rw-r--r--vendor/gitignore/SCons.gitignore2
-rw-r--r--vendor/gitignore/Sass.gitignore2
-rw-r--r--vendor/gitignore/Scala.gitignore17
-rw-r--r--vendor/gitignore/Scheme.gitignore7
-rw-r--r--vendor/gitignore/Scrivener.gitignore7
-rw-r--r--vendor/gitignore/Sdcc.gitignore8
-rw-r--r--vendor/gitignore/SeamGen.gitignore26
-rw-r--r--vendor/gitignore/SketchUp.gitignore1
-rw-r--r--vendor/gitignore/Smalltalk.gitignore18
-rw-r--r--vendor/gitignore/Stella.gitignore12
-rw-r--r--vendor/gitignore/SugarCRM.gitignore25
-rw-r--r--vendor/gitignore/Swift.gitignore65
-rw-r--r--vendor/gitignore/Symfony.gitignore48
-rw-r--r--vendor/gitignore/SymphonyCMS.gitignore6
-rw-r--r--vendor/gitignore/TeX.gitignore183
-rw-r--r--vendor/gitignore/Terraform.gitignore3
-rw-r--r--vendor/gitignore/Textpattern.gitignore11
-rw-r--r--vendor/gitignore/TurboGears2.gitignore20
-rw-r--r--vendor/gitignore/Typo3.gitignore20
-rw-r--r--vendor/gitignore/Umbraco.gitignore19
-rw-r--r--vendor/gitignore/Unity.gitignore30
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore63
-rw-r--r--vendor/gitignore/VVVV.gitignore6
-rw-r--r--vendor/gitignore/VisualStudio.gitignore253
-rw-r--r--vendor/gitignore/Waf.gitignore4
-rw-r--r--vendor/gitignore/WordPress.gitignore18
-rw-r--r--vendor/gitignore/Xojo.gitignore11
-rw-r--r--vendor/gitignore/Yeoman.gitignore6
-rw-r--r--vendor/gitignore/Yii.gitignore6
-rw-r--r--vendor/gitignore/ZendFramework.gitignore25
-rw-r--r--vendor/gitignore/Zephir.gitignore26
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml18
-rw-r--r--vendor/gitlab-ci-yml/LICENSE21
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml102
-rw-r--r--vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/Brunch.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/Doxygen.gitlab-ci.yml13
-rw-r--r--vendor/gitlab-ci-yml/Pages/HTML.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/Harp.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml11
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hyde.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml24
-rw-r--r--vendor/gitlab-ci-yml/Pages/Lektor.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/Metalsmith.gitlab-ci.yml17
-rw-r--r--vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/Nanoc.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/Octopress.gitlab-ci.yml15
-rw-r--r--vendor/gitlab-ci-yml/Pages/Pelican.gitlab-ci.yml10
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml37
-rw-r--r--vendor/gitlab-ci-yml/Rust.gitlab-ci.yml23
-rw-r--r--vendor/gitlab-ci-yml/Scala.gitlab-ci.yml22
2770 files changed, 81920 insertions, 18758 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000000..2e88b7aa0a9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,3 @@
+We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly.
+
+We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..c3b04026440
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,3 @@
+Thank you for taking the time to contribute back to GitLab!
+
+Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
diff --git a/.gitignore b/.gitignore
index 8f861d76a37..ce6a363fe35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,46 +4,46 @@
.bundle
.chef
.directory
-.envrc
-.gitlab_shell_secret
+/.envrc
+/.gitlab_shell_secret
.idea
-.rbenv-version
+/.rbenv-version
.rbx/
-.ruby-gemset
-.ruby-version
-.rvmrc
+/.ruby-gemset
+/.ruby-version
+/.rvmrc
.sass-cache/
-.secret
-.vagrant
-.byebug_history
-Vagrantfile
-backups/*
-config/aws.yml
-config/database.yml
-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
-db/data.yml
-doc/code/*
-dump.rdb
-log/*.log*
-nohup.out
-public/assets/
-public/uploads.*
-public/uploads/
-shared/artifacts/
-rails_best_practices_output.html
+/.secret
+/.vagrant
+/.byebug_history
+/Vagrantfile
+/backups/*
+/config/aws.yml
+/config/database.yml
+/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
+/db/data.yml
+/doc/code/*
+/dump.rdb
+/log/*.log*
+/nohup.out
+/public/assets/
+/public/uploads.*
+/public/uploads/
+/shared/artifacts/
+/rails_best_practices_output.html
/tags
-tmp/
-vendor/bundle/*
-builds/*
-shared/*
+/tmp/*
+/vendor/bundle/*
+/builds/*
+/shared/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 85730e1b687..ff8aa351226 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,240 +2,212 @@ image: "ruby:2.1"
services:
- mysql:latest
- - redis:latest
+ - redis:alpine
cache:
key: "ruby21"
paths:
- - vendor
+ - vendor/apt
+ - vendor/ruby
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
+ RAILS_ENV: "test"
+ SIMPLECOV: "true"
+ USE_DB: "true"
+ USE_BUNDLE_INSTALL: "true"
+ GIT_DEPTH: "20"
before_script:
- source ./scripts/prepare_build.sh
- - ruby -v
- - which ruby
- - retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- - touch log/application.log
- - touch log/test.log
- - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
+ - bundle --version
+ - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
+ - retry gem install knapsack
+ - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages:
+- prepare
- test
-- notifications
+- post-test
-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
-
-spec:api:
- stage: test
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
-
-spec:models:
- stage: test
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
-
-spec:lib:
- stage: test
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
-
-spec:services:
- stage: test
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
-
-spec:other:
- stage: test
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
-
-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
-
-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
+# Prepare and merge knapsack tests
-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
-
-teaspoon:
- stage: test
- script:
- - RAILS_ENV=test bundle exec teaspoon
-
-rubocop:
- stage: test
- script:
- - bundle exec rubocop
-
-scss-lint:
- stage: test
- script:
- - bundle exec rake scss_lint
-
-brakeman:
- stage: test
- script:
- - bundle exec rake brakeman
-
-flog:
- stage: test
- script:
- - bundle exec rake flog
-
-flay:
- stage: test
- script:
- - bundle exec rake flay
-
-bundler:audit:
- stage: test
- only:
- - master
- script:
- - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
-
-db-migrate-reset:
- stage: test
- script:
- - RAILS_ENV=test bundle exec rake db:migrate:reset
-
-# 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
+.knapsack-state: &knapsack-state
+ services: []
+ variables:
+ USE_DB: "false"
+ USE_BUNDLE_INSTALL: "false"
cache:
- key: "ruby22"
+ key: "knapsack"
paths:
- - vendor
-
-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"
+ - knapsack/
+ artifacts:
paths:
- - vendor
+ - knapsack/
-spec:models:ruby22:
- stage: test
- image: ruby:2.2
- only:
- - master
+knapsack:
+ <<: *knapsack-state
+ stage: prepare
script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
- cache:
- key: "ruby22"
- paths:
- - vendor
+ - mkdir -p knapsack/
+ - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
+ - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
-spec:lib:ruby22:
- stage: test
- image: ruby:2.2
- only:
- - master
+update-knapsack:
+ <<: *knapsack-state
+ stage: post-test
script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
- cache:
- key: "ruby22"
- paths:
- - vendor
-
-spec:services:ruby22:
- stage: test
- image: ruby:2.2
+ - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
+ - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
+ - rm -f knapsack/*_node_*.json
only:
- - master
- script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
- cache:
- key: "ruby22"
- paths:
- - vendor
+ - master
+
+# Execute all testing suites
-spec:other:ruby22:
+.rspec-knapsack: &rspec-knapsack
stage: test
- image: ruby:2.2
- only:
- - master
script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
- cache:
- key: "ruby22"
+ - bundle exec rake assets:precompile 2>/dev/null
+ - JOB_NAME=( $CI_BUILD_NAME )
+ - export CI_NODE_INDEX=${JOB_NAME[1]}
+ - export CI_NODE_TOTAL=${JOB_NAME[2]}
+ - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_GENERATE_REPORT=true
+ - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
+ - knapsack rspec
+ artifacts:
paths:
- - vendor
+ - knapsack/
-spinach:project:half:ruby22:
+.spinach-knapsack: &spinach-knapsack
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"
+ - bundle exec rake assets:precompile 2>/dev/null
+ - JOB_NAME=( $CI_BUILD_NAME )
+ - export CI_NODE_INDEX=${JOB_NAME[1]}
+ - export CI_NODE_TOTAL=${JOB_NAME[2]}
+ - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_GENERATE_REPORT=true
+ - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
+ - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ artifacts:
paths:
- - vendor
-
-spinach:project:rest:ruby22:
- stage: test
- image: ruby:2.2
+ - knapsack/
+
+rspec 0 20: *rspec-knapsack
+rspec 1 20: *rspec-knapsack
+rspec 2 20: *rspec-knapsack
+rspec 3 20: *rspec-knapsack
+rspec 4 20: *rspec-knapsack
+rspec 5 20: *rspec-knapsack
+rspec 6 20: *rspec-knapsack
+rspec 7 20: *rspec-knapsack
+rspec 8 20: *rspec-knapsack
+rspec 9 20: *rspec-knapsack
+rspec 10 20: *rspec-knapsack
+rspec 11 20: *rspec-knapsack
+rspec 12 20: *rspec-knapsack
+rspec 13 20: *rspec-knapsack
+rspec 14 20: *rspec-knapsack
+rspec 15 20: *rspec-knapsack
+rspec 16 20: *rspec-knapsack
+rspec 17 20: *rspec-knapsack
+rspec 18 20: *rspec-knapsack
+rspec 19 20: *rspec-knapsack
+
+spinach 0 10: *spinach-knapsack
+spinach 1 10: *spinach-knapsack
+spinach 2 10: *spinach-knapsack
+spinach 3 10: *spinach-knapsack
+spinach 4 10: *spinach-knapsack
+spinach 5 10: *spinach-knapsack
+spinach 6 10: *spinach-knapsack
+spinach 7 10: *spinach-knapsack
+spinach 8 10: *spinach-knapsack
+spinach 9 10: *spinach-knapsack
+
+# Execute all testing suites against Ruby 2.3
+.ruby-23: &ruby-23
+ image: "ruby:2.3"
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
+ - master
cache:
- key: "ruby22"
+ key: "ruby-23"
paths:
- - vendor
+ - vendor/apt
+ - vendor/ruby
+
+.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
+ <<: *rspec-knapsack
+ <<: *ruby-23
+
+.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
+ <<: *spinach-knapsack
+ <<: *ruby-23
+
+rspec 0 20 ruby23: *rspec-knapsack-ruby23
+rspec 1 20 ruby23: *rspec-knapsack-ruby23
+rspec 2 20 ruby23: *rspec-knapsack-ruby23
+rspec 3 20 ruby23: *rspec-knapsack-ruby23
+rspec 4 20 ruby23: *rspec-knapsack-ruby23
+rspec 5 20 ruby23: *rspec-knapsack-ruby23
+rspec 6 20 ruby23: *rspec-knapsack-ruby23
+rspec 7 20 ruby23: *rspec-knapsack-ruby23
+rspec 8 20 ruby23: *rspec-knapsack-ruby23
+rspec 9 20 ruby23: *rspec-knapsack-ruby23
+rspec 10 20 ruby23: *rspec-knapsack-ruby23
+rspec 11 20 ruby23: *rspec-knapsack-ruby23
+rspec 12 20 ruby23: *rspec-knapsack-ruby23
+rspec 13 20 ruby23: *rspec-knapsack-ruby23
+rspec 14 20 ruby23: *rspec-knapsack-ruby23
+rspec 15 20 ruby23: *rspec-knapsack-ruby23
+rspec 16 20 ruby23: *rspec-knapsack-ruby23
+rspec 17 20 ruby23: *rspec-knapsack-ruby23
+rspec 18 20 ruby23: *rspec-knapsack-ruby23
+rspec 19 20 ruby23: *rspec-knapsack-ruby23
+
+spinach 0 10 ruby23: *spinach-knapsack-ruby23
+spinach 1 10 ruby23: *spinach-knapsack-ruby23
+spinach 2 10 ruby23: *spinach-knapsack-ruby23
+spinach 3 10 ruby23: *spinach-knapsack-ruby23
+spinach 4 10 ruby23: *spinach-knapsack-ruby23
+spinach 5 10 ruby23: *spinach-knapsack-ruby23
+spinach 6 10 ruby23: *spinach-knapsack-ruby23
+spinach 7 10 ruby23: *spinach-knapsack-ruby23
+spinach 8 10 ruby23: *spinach-knapsack-ruby23
+spinach 9 10 ruby23: *spinach-knapsack-ruby23
+
+# Other generic tests
+
+.exec: &exec
+ stage: test
+ script:
+ - bundle exec $CI_BUILD_NAME
+
+teaspoon: *exec
+rubocop: *exec
+rake scss_lint: *exec
+rake brakeman: *exec
+rake flog: *exec
+rake flay: *exec
+rake db:migrate:reset: *exec
+license_finder: *exec
-spinach:other:ruby22:
+bundler:audit:
stage: test
- image: ruby:2.2
only:
- - master
+ - 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
+ - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+
+# Notify slack in the end
notify:slack:
- stage: notifications
+ stage: post-test
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
diff --git a/.rubocop.yml b/.rubocop.yml
index 9f179efa3ce..cd13f581517 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,3 +1,7 @@
+require:
+ - rubocop-rspec
+ - ./rubocop/rubocop
+
AllCops:
TargetRubyVersion: 2.1
# Cop names are not displayed in offense messages by default. Change behavior
@@ -11,7 +15,8 @@ AllCops:
# Exclude some GitLab files
Exclude:
- 'vendor/**/*'
- - 'db/**/*'
+ - 'db/*'
+ - 'db/fixtures/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'lib/backup/**/*'
@@ -21,6 +26,7 @@ AllCops:
- 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb'
+ - 'generator_templates/**/*'
##################### Style ##################################
@@ -56,7 +62,7 @@ Style/AndOr:
# Use `Array#join` instead of `Array#*`.
Style/ArrayJoin:
- Enabled: false
+ Enabled: true
# Use only ascii symbols in comments.
Style/AsciiComments:
@@ -68,7 +74,7 @@ Style/AsciiIdentifiers:
# Checks for uses of Module#attr.
Style/Attr:
- Enabled: false
+ Enabled: true
# Avoid the use of BEGIN blocks.
Style/BeginBlock:
@@ -80,7 +86,7 @@ Style/BarePercentLiterals:
# Do not use block comments.
Style/BlockComments:
- Enabled: false
+ Enabled: true
# Put end statement of multiline block on its own line.
Style/BlockEndNewline:
@@ -121,7 +127,7 @@ Style/ClassCheck:
# Use self when defining module/class methods.
Style/ClassMethods:
- Enabled: false
+ Enabled: true
# Avoid the use of class variables.
Style/ClassVars:
@@ -151,7 +157,7 @@ Style/ConstantName:
# Use def with parentheses when there are arguments.
Style/DefWithParentheses:
- Enabled: false
+ Enabled: true
# Checks for use of deprecated Hash methods.
Style/DeprecatedHashMethods:
@@ -187,11 +193,11 @@ Style/EmptyLineBetweenDefs:
# Don't use several empty lines in a row.
Style/EmptyLines:
- Enabled: false
+ Enabled: true
# Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier:
- Enabled: false
+ Enabled: true
# Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody:
@@ -215,15 +221,15 @@ Style/EmptyLiteral:
# Avoid the use of END blocks.
Style/EndBlock:
- Enabled: false
+ Enabled: true
# Use Unix-style line endings.
Style/EndOfLine:
- Enabled: false
+ Enabled: true
# Favor the use of Fixnum#even? && Fixnum#odd?
Style/EvenOdd:
- Enabled: false
+ Enabled: true
# Do not use unnecessary spacing.
Style/ExtraSpacing:
@@ -231,15 +237,20 @@ Style/ExtraSpacing:
# Use snake_case for source file names.
Style/FileName:
- Enabled: false
+ Enabled: true
+
+# Checks for a line break before the first parameter in a multi-line method
+# parameter definition.
+Style/FirstMethodParameterLineBreak:
+ Enabled: true
# Checks for flip flops.
Style/FlipFlop:
- Enabled: false
+ Enabled: true
# Checks use of for or each in multiline loops.
Style/For:
- Enabled: false
+ Enabled: true
# Enforce the use of Kernel#sprintf, Kernel#format or String#%.
Style/FormatString:
@@ -247,7 +258,7 @@ Style/FormatString:
# Do not introduce global variables.
Style/GlobalVars:
- Enabled: false
+ Enabled: true
# Check for conditionals that can be replaced with guard clauses.
Style/GuardClause:
@@ -268,7 +279,7 @@ Style/IfUnlessModifier:
# Do not use if x; .... Use the ternary operator instead.
Style/IfWithSemicolon:
- Enabled: false
+ Enabled: true
# Checks that conditional statements do not have an identical line at the
# end of each branch, which can validly be moved out of the conditional.
@@ -276,9 +287,9 @@ Style/IdenticalConditionalBranches:
Enabled: false
# Checks the indentation of the first line of the right-hand-side of a
-# multi-line assignment.
+# multi-line assignment.
Style/IndentAssignment:
- Enabled: false
+ Enabled: true
# Keep indentation straight.
Style/IndentationConsistency:
@@ -298,7 +309,7 @@ Style/IndentHash:
# Use Kernel#loop for infinite loops.
Style/InfiniteLoop:
- Enabled: false
+ Enabled: true
# Use the new lambda literal syntax for single-line blocks.
Style/Lambda:
@@ -306,11 +317,11 @@ Style/Lambda:
# Use lambda.call(...) instead of lambda.(...).
Style/LambdaCall:
- Enabled: false
+ Enabled: true
# Comments should start with a space.
Style/LeadingCommentSpace:
- Enabled: false
+ Enabled: true
# Use \ instead of + or << to concatenate two string literals at line end.
Style/LineEndConcatenation:
@@ -322,33 +333,56 @@ Style/MethodCallParentheses:
# Checks if the method definitions have or don't have parentheses.
Style/MethodDefParentheses:
- Enabled: false
+ Enabled: true
# Use the configured style when naming methods.
Style/MethodName:
- Enabled: false
+ Enabled: true
# Checks for usage of `extend self` in modules.
Style/ModuleFunction:
Enabled: false
+# Checks that the closing brace in an array literal is either on the same line
+# as the last array element, or a new line.
+Style/MultilineArrayBraceLayout:
+ Enabled: false
+ EnforcedStyle: symmetrical
+
# Avoid multi-line chains of blocks.
Style/MultilineBlockChain:
- Enabled: false
+ Enabled: true
# Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout:
Enabled: true
+# Checks that the closing brace in a hash literal is either on the same line as
+# the last hash element, or a new line.
+Style/MultilineHashBraceLayout:
+ Enabled: false
+ EnforcedStyle: symmetrical
+
# Do not use then for multi-line if/unless.
Style/MultilineIfThen:
+ Enabled: true
+
+# Checks that the closing brace in a method call is either on the same line as
+# the last method argument, or a new line.
+Style/MultilineMethodCallBraceLayout:
Enabled: false
+ EnforcedStyle: symmetrical
# Checks indentation of method calls with the dot operator that span more than
# one line.
Style/MultilineMethodCallIndentation:
Enabled: false
+# Checks that the closing brace in a method definition is symmetrical with
+# respect to the opening brace and the method parameters.
+Style/MultilineMethodDefinitionBraceLayout:
+ Enabled: false
+
# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
Enabled: false
@@ -363,7 +397,7 @@ Style/MutableConstant:
# Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf:
- Enabled: false
+ Enabled: true
# Favor until over while for negative conditions.
Style/NegatedWhile:
@@ -371,7 +405,7 @@ Style/NegatedWhile:
# Avoid using nested modifiers.
Style/NestedModifier:
- Enabled: false
+ Enabled: true
# Parenthesize method calls which are nested inside the argument list of
# another parenthesized method call.
@@ -408,7 +442,7 @@ Style/OneLineConditional:
# When defining binary operators, name the argument other.
Style/OpMethod:
- Enabled: false
+ Enabled: true
# Check for simple usages of parallel assignment. It will only warn when
# the number of variables matches on both sides of the assignment.
@@ -455,10 +489,9 @@ Style/RedundantException:
Style/RedundantFreeze:
Enabled: false
-# TODO: Enable RedundantParentheses Cop.
# Checks for parentheses that seem not to serve any purpose.
Style/RedundantParentheses:
- Enabled: false
+ Enabled: true
# Don't use return where it's not required.
Style/RedundantReturn:
@@ -484,11 +517,12 @@ Style/SelfAssignment:
# Don't use semicolons to terminate expressions.
Style/Semicolon:
- Enabled: false
+ Enabled: true
# Checks for proper usage of fail and raise.
Style/SignalException:
- Enabled: false
+ EnforcedStyle: only_raise
+ Enabled: true
# Enforces the names of some block params.
Style/SingleLineBlockParams:
@@ -500,38 +534,37 @@ Style/SingleLineMethods:
# Use spaces after colons.
Style/SpaceAfterColon:
- Enabled: false
+ Enabled: true
# Use spaces after commas.
Style/SpaceAfterComma:
- Enabled: false
+ Enabled: true
# Do not put a space between a method name and the opening parenthesis in a
# method definition.
Style/SpaceAfterMethodName:
- Enabled: false
+ Enabled: true
# Tracks redundant space after the ! operator.
Style/SpaceAfterNot:
- Enabled: false
+ Enabled: true
# Use spaces after semicolons.
Style/SpaceAfterSemicolon:
- Enabled: false
+ Enabled: true
# Checks that the equals signs in parameter default assignments have or don't
# have surrounding space depending on configuration.
Style/SpaceAroundEqualsInParameterDefault:
Enabled: false
-# TODO: Enable SpaceAroundKeyword Cop.
# Use a space around keywords if appropriate.
Style/SpaceAroundKeyword:
- Enabled: false
+ Enabled: true
# Use a single space around operators.
Style/SpaceAroundOperators:
- Enabled: false
+ Enabled: true
# Checks that the left block brace has or doesn't have space before it.
Style/SpaceBeforeBlockBraces:
@@ -539,11 +572,11 @@ Style/SpaceBeforeBlockBraces:
# No spaces before commas.
Style/SpaceBeforeComma:
- Enabled: false
+ Enabled: true
# Checks for missing space between code and a comment on the same line.
Style/SpaceBeforeComment:
- Enabled: false
+ Enabled: true
# Checks that exactly one space is used between a method name and the first
# argument for method calls without parentheses.
@@ -552,7 +585,7 @@ Style/SpaceBeforeFirstArg:
# No spaces before semicolons.
Style/SpaceBeforeSemicolon:
- Enabled: false
+ Enabled: true
# Checks that block braces have or don't have surrounding space.
# For blocks taking parameters, checks that the left brace has or doesn't
@@ -574,11 +607,12 @@ Style/SpaceInsideParens:
# No spaces inside range literals.
Style/SpaceInsideRangeLiteral:
- Enabled: false
+ Enabled: true
# Checks for padding/surrounding spaces inside string interpolation.
Style/SpaceInsideStringInterpolation:
- Enabled: false
+ EnforcedStyle: no_space
+ Enabled: true
# Avoid Perl-style global variables.
Style/SpecialGlobalVars:
@@ -586,7 +620,8 @@ Style/SpecialGlobalVars:
# Check for the usage of parentheses around stabby lambda arguments.
Style/StabbyLambdaParentheses:
- Enabled: false
+ EnforcedStyle: require_parentheses
+ Enabled: true
# Checks if uses of quotes match the configured preference.
Style/StringLiterals:
@@ -599,7 +634,9 @@ Style/StringLiteralsInInterpolation:
# Checks if configured preferred methods are used over non-preferred.
Style/StringMethods:
- Enabled: false
+ PreferredMethods:
+ intern: to_sym
+ Enabled: true
# Use %i or %I for arrays of symbols.
Style/SymbolArray:
@@ -644,7 +681,7 @@ Style/UnlessElse:
# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
- Enabled: false
+ Enabled: true
# TODO: Enable UnneededInterpolation Cop.
# Checks for strings that are just an interpolated expression.
@@ -657,23 +694,24 @@ Style/UnneededPercentQ:
# Don't interpolate global, instance and class variables directly in strings.
Style/VariableInterpolation:
- Enabled: false
+ Enabled: true
# Use the configured style when naming variables.
Style/VariableName:
- Enabled: false
+ EnforcedStyle: snake_case
+ Enabled: true
# Use when x then ... for one-line cases.
Style/WhenThen:
- Enabled: false
+ Enabled: true
# Checks for redundant do after while or until.
Style/WhileUntilDo:
- Enabled: false
+ Enabled: true
# Favor modifier while/until usage when you have a single-line body.
Style/WhileUntilModifier:
- Enabled: false
+ Enabled: true
# Use %w or %W for arrays of words.
Style/WordArray:
@@ -736,7 +774,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation
# without parentheses.
Lint/AmbiguousOperator:
- Enabled: false
+ Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses.
@@ -749,28 +787,28 @@ Lint/AssignmentInCondition:
# Align block ends correctly.
Lint/BlockAlignment:
- Enabled: false
+ Enabled: true
# Default values in optional keyword arguments and optional ordinal arguments
# should not refer back to the name of the argument.
Lint/CircularArgumentReference:
- Enabled: false
+ Enabled: true
# Checks for condition placed in a confusing position relative to the keyword.
Lint/ConditionPosition:
- Enabled: false
+ Enabled: true
# Check for debugger calls.
Lint/Debugger:
- Enabled: false
+ Enabled: true
# Align ends corresponding to defs correctly.
Lint/DefEndAlignment:
- Enabled: false
+ Enabled: true
# Check for deprecated class method calls.
Lint/DeprecatedClassMethods:
- Enabled: false
+ Enabled: true
# Check for duplicate method definitions.
Lint/DuplicateMethods:
@@ -782,15 +820,15 @@ Lint/DuplicatedKey:
# Check for immutable argument given to each_with_object.
Lint/EachWithObjectArgument:
- Enabled: false
+ Enabled: true
# Check for odd code arrangement in an else block.
Lint/ElseLayout:
- Enabled: false
+ Enabled: true
# Checks for empty ensure block.
Lint/EmptyEnsure:
- Enabled: false
+ Enabled: true
# Checks for empty string interpolation.
Lint/EmptyInterpolation:
@@ -798,37 +836,36 @@ Lint/EmptyInterpolation:
# Align ends correctly.
Lint/EndAlignment:
- Enabled: false
+ Enabled: true
# END blocks should not be placed inside method definitions.
Lint/EndInMethod:
- Enabled: false
+ Enabled: true
# Do not use return in an ensure block.
Lint/EnsureReturn:
- Enabled: false
+ Enabled: true
# The use of eval represents a serious security risk.
Lint/Eval:
- Enabled: false
+ Enabled: true
# Catches floating-point literals too large or small for Ruby to represent.
Lint/FloatOutOfRange:
- Enabled: false
+ Enabled: true
# The number of parameters to format/sprint must match the fields.
Lint/FormatParameterMismatch:
- Enabled: false
+ Enabled: true
# Don't suppress exception.
Lint/HandleExceptions:
Enabled: false
-# TODO: Enable ImplicitStringConcatenation Cop.
# Checks for adjacent string literals on the same line, which could better be
# represented as a single string literal.
Lint/ImplicitStringConcatenation:
- Enabled: false
+ Enabled: true
# TODO: Enable IneffectiveAccessModifier Cop.
# Checks for attempts to use `private` or `protected` to set the visibility
@@ -839,15 +876,15 @@ Lint/IneffectiveAccessModifier:
# Checks for invalid character literals with a non-escaped whitespace
# character.
Lint/InvalidCharacterLiteral:
- Enabled: false
+ Enabled: true
# Checks of literals used in conditions.
Lint/LiteralInCondition:
- Enabled: false
+ Enabled: true
# Checks for literals used in interpolation.
Lint/LiteralInInterpolation:
- Enabled: false
+ Enabled: true
# Use Kernel#loop with break rather than begin/end/until or begin/end/while
# for post-loop tests.
@@ -856,11 +893,11 @@ Lint/Loop:
# Do not use nested method definitions.
Lint/NestedMethodDefinition:
- Enabled: false
+ Enabled: true
# Do not omit the accumulator when calling `next` in a `reduce`/`inject` block.
Lint/NextWithoutAccumulator:
- Enabled: false
+ Enabled: true
# Checks for method calls with a space before the opening parenthesis.
Lint/ParenthesesAsGroupedExpression:
@@ -869,11 +906,11 @@ Lint/ParenthesesAsGroupedExpression:
# Checks for `rand(1)` calls. Such calls always return `0` and most likely
# a mistake.
Lint/RandOne:
- Enabled: false
+ Enabled: true
# Use parentheses in the method call to avoid confusion about precedence.
Lint/RequireParentheses:
- Enabled: false
+ Enabled: true
# Avoid rescuing the Exception class.
Lint/RescueException:
@@ -908,7 +945,7 @@ Lint/UnusedMethodArgument:
# Unreachable code.
Lint/UnreachableCode:
- Enabled: false
+ Enabled: true
# Checks for useless access modifiers.
Lint/UselessAccessModifier:
@@ -920,33 +957,31 @@ Lint/UselessAssignment:
# Checks for comparison of something with itself.
Lint/UselessComparison:
- Enabled: false
+ Enabled: true
# Checks for useless `else` in `begin..end` without `rescue`.
Lint/UselessElseWithoutRescue:
- Enabled: false
+ Enabled: true
# Checks for useless setter call to a local variable.
Lint/UselessSetterCall:
- Enabled: false
+ Enabled: true
# Possible use of operator/literal/variable in void context.
Lint/Void:
- Enabled: false
+ Enabled: true
##################### Performance ############################
-# TODO: Enable Casecmp Cop.
# Use `casecmp` rather than `downcase ==`.
Performance/Casecmp:
- Enabled: false
+ Enabled: true
-# TODO: Enable DoubleStartEndWith Cop.
# Use `str.{start,end}_with?(x, ..., y, ...)` instead of
# `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`.
Performance/DoubleStartEndWith:
- Enabled: false
+ Enabled: true
# TODO: Enable EndWith Cop.
# Use `end_with?` instead of a regex match anchored to the end of a string.
@@ -957,10 +992,9 @@ Performance/EndWith:
Performance/LstripRstrip:
Enabled: true
-# TODO: Enable RangeInclude Cop.
# Use `Range#cover?` instead of `Range#include?`.
Performance/RangeInclude:
- Enabled: false
+ Enabled: true
# TODO: Enable RedundantBlockCall Cop.
# Use `yield` instead of `block.call`.
@@ -980,26 +1014,24 @@ Performance/RedundantMerge:
MaxKeyValuePairs: 2
Enabled: false
-# TODO: Enable RedundantSortBy Cop.
# Use `sort` instead of `sort_by { |x| x }`.
Performance/RedundantSortBy:
- Enabled: false
+ Enabled: true
-# TODO: Enable StartWith Cop.
# Use `start_with?` instead of a regex match anchored to the beginning of a
# string.
Performance/StartWith:
- Enabled: false
+ Enabled: true
+
# Use `tr` instead of `gsub` when you are replacing the same number of
# characters. Use `delete` instead of `gsub` when you are deleting
# characters.
Performance/StringReplacement:
- Enabled: false
+ Enabled: true
-# TODO: Enable TimesMap Cop.
# Checks for `.times.map` calls.
Performance/TimesMap:
- Enabled: false
+ Enabled: true
##################### Rails ##################################
@@ -1024,11 +1056,11 @@ Rails/Delegate:
# Prefer `find_by` over `where.first`.
Rails/FindBy:
- Enabled: false
+ Enabled: true
# Prefer `all.find_each` over `all.find`.
Rails/FindEach:
- Enabled: false
+ Enabled: true
# Prefer has_many :through to has_and_belongs_to_many.
Rails/HasAndBelongsToMany:
@@ -1040,7 +1072,7 @@ Rails/Output:
# Checks for incorrect grammar when using methods like `3.day.ago`.
Rails/PluralizationGrammar:
- Enabled: false
+ Enabled: true
# Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`.
Rails/ReadWriteAttribute:
@@ -1048,7 +1080,7 @@ Rails/ReadWriteAttribute:
# Checks the arguments of ActiveRecord scopes.
Rails/ScopeArgs:
- Enabled: false
+ Enabled: true
# Checks the correct usage of time zone aware methods.
# http://danilenko.org/2012/7/6/rails_timezones
@@ -1058,3 +1090,68 @@ Rails/TimeZone:
# Use validates :attribute, hash of validations.
Rails/Validation:
Enabled: false
+
+Rails/UniqBeforePluck:
+ Enabled: false
+
+##################### RSpec ##################################
+
+# Check that instances are not being stubbed globally.
+RSpec/AnyInstance:
+ Enabled: false
+
+# Check that the first argument to the top level describe is the tested class or
+# module.
+RSpec/DescribeClass:
+ Enabled: false
+
+# Use `described_class` for tested class / module.
+RSpec/DescribeMethod:
+ Enabled: false
+
+# Checks that the second argument to top level describe is the tested method
+# name.
+RSpec/DescribedClass:
+ Enabled: false
+
+# Checks for long example.
+RSpec/ExampleLength:
+ Enabled: false
+ Max: 5
+
+# Do not use should when describing your tests.
+RSpec/ExampleWording:
+ Enabled: false
+ CustomTransform:
+ be: is
+ have: has
+ not: does not
+ IgnoredWords: []
+
+# Checks the file and folder naming of the spec file.
+RSpec/FilePath:
+ Enabled: false
+ CustomTransform:
+ RuboCop: rubocop
+ RSpec: rspec
+
+# Checks if there are focused specs.
+RSpec/Focus:
+ Enabled: true
+
+# Checks for the usage of instance variables.
+RSpec/InstanceVariable:
+ Enabled: false
+
+# Checks for multiple top-level describes.
+RSpec/MultipleDescribes:
+ Enabled: false
+
+# Enforces the usage of the same method on all negative message expectations.
+RSpec/NotToNot:
+ EnforcedStyle: not_to
+ Enabled: true
+
+# Prefer using verifying doubles over normal doubles.
+RSpec/VerifiedDoubles:
+ Enabled: false
diff --git a/app/views/projects/notes/_commit_discussion.html.haml b/.vagrant_enabled
index e69de29bb2d..e69de29bb2d 100644
--- a/app/views/projects/notes/_commit_discussion.html.haml
+++ b/.vagrant_enabled
diff --git a/CHANGELOG b/CHANGELOG
index 54c79551b1d..12906398a94 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,57 +1,491 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.8.0 (unreleased)
+v 8.10.0 (unreleased)
+ - Fix commit builds API, return all builds for all pipelines for given commit. !4849
+ - Replace Haml with Hamlit to make view rendering faster. !3666
+ - Refactor repository paths handling to allow multiple git mount points
+ - Add Application Setting to configure default Repository Path for new projects
+ - Wrap code blocks on Activies and Todos page. !4783 (winniehell)
+ - Align flash messages with left side of page content !4959 (winniehell)
+ - Display last commit of deleted branch in push events !4699 (winniehell)
+ - Escape file extension when parsing search results !5141 (winniehell)
+ - Apply the trusted_proxies config to the rack request object for use with rack_attack
+ - Add Sidekiq queue duration to transaction metrics.
+ - Add a new column `artifacts_size` to table `ci_builds` !4964
+ - Let Workhorse serve format-patch diffs
+ - Make images fit to the size of the viewport !4810
+ - Fix check for New Branch button on Issue page !4630 (winniehell)
+ - Fix MR-auto-close text added to description. !4836
+ - Fix issue, preventing users w/o push access to sort tags !5105 (redetection)
+ - Add Spring EmojiOne updates.
+ - Fix viewing notification settings when a project is pending deletion
+ - Fix pagination when sorting by columns with lots of ties (like priority)
+ - Updated project header design
+ - Exclude email check from the standard health check
+ - Updated layout for Projects, Groups, Users on Admin area !4424
+ - Fix changing issue state columns in milestone view
+ - Add notification settings dropdown for groups
+ - Wildcards for protected branches. !4665
+ - Allow importing from Github using Personal Access Tokens. (Eric K Idema)
+ - API: Todos !3188 (Robert Schilling)
+ - API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling)
+ - Add "Enabled Git access protocols" to Application Settings
+ - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
+ - PipelinesFinder uses git cache data
+ - Throttle the update of `project.pushes_since_gc` to 1 minute.
+ - Check for conflicts with existing Project's wiki path when creating a new project.
+ - Show last push widget in upstream after push to fork
+ - Don't instantiate a git tree on Projects show default view
+ - Bump Rinku to 2.0.0
+ - Remove unused front-end variable -> default_issues_tracker
+ - Better caching of git calls on ProjectsController#show.
+ - Add API endpoint for a group issues !4520 (mahcsig)
+ - Add Bugzilla integration !4930 (iamtjg)
+ - Instrument Rinku usage
+ - Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
+ - RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
+ - Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
+ - Set import_url validation to be more strict
+ - Memoize MR merged/closed events retrieval
+ - Don't render discussion notes when requesting diff tab through AJAX
+ - Add basic system information like memory and disk usage to the admin panel
+ - Don't garbage collect commits that have related DB records like comments
+ - More descriptive message for git hooks and file locks
+ - Handle custom Git hook result in GitLab UI
+ - Allow '?', or '&' for label names
+ - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests
+ - Add date when user joined the team on the member page
+ - Fix 404 redirect after validation fails importing a GitLab project
+ - Added setting to set new users by default as external !4545 (Dravere)
+
+v 8.9.5
+ - Add more debug info to import/export and memory killer. !5108
+ - Fixed avatar alignment in new MR view. !5095
+ - Fix diff comments not showing up in activity feed. !5069
+ - Add index on both Award Emoji user and name. !5061
+ - Downgrade to Redis 3.2.2 due to massive memory leak with Sidekiq. !5056
+ - Re-enable import button when import process fails due to namespace already being taken. !5053
+ - Fix snippets comments not displayed. !5045
+ - Fix emoji paths in relative root configurations. !5027
+ - Fix issues importing events in Import/Export. !4987
+ - Fixed 'use shortcuts' button on docs. !4979
+ - Admin should be able to turn shared runners into specific ones. !4961
+ - Update RedCloth to 4.3.2 for CVE-2012-6684. !4929 (Takuya Noguchi)
+ - Improve the request / withdraw access button. !4860
+
+v 8.9.4
+ - Fix privilege escalation issue with OAuth external users.
+ - Ensure references to private repos aren't shown to logged-out users.
+ - Fixed search field blur not removing focus. !4704
+ - Resolve "Sub nav isn't showing on file view". !4890
+ - Fixes middle click and double request when navigating through the file browser. !4891
+ - Fixed URL on label button when filtering. !4897
+ - Fixed commit avatar alignment. !4933
+ - Do not show build retry link when build is active. !4967
+ - Fix restore Rake task warning message output. !4980
+ - Handle external issues in IssueReferenceFilter. !4988
+ - Expiry date on pinned nav cookie. !5009
+ - Updated breakpoint for sidebar pinning. !5019
+
+v 8.9.3
+ - Fix encrypted data backwards compatibility after upgrading attr_encrypted gem. !4963
+ - Fix rendering of commit notes. !4953
+ - Resolve "Pin should show up at 1280px min". !4947
+ - Switched mobile button icons to ellipsis and angle. !4944
+ - Correctly returns todo ID after creating todo. !4941
+ - Better debugging for memory killer middleware. !4936
+ - Remove duplicate new page btn from edit wiki. !4904
+ - Use clock_gettime for all performance timestamps. !4899
+ - Use memorized tags array when searching tags by name. !4859
+ - Fixed avatar alignment in new MR view. !4901
+ - Removed fade when filtering results. !4932
+ - Fix missing avatar on system notes. !4954
+ - Reduce overhead and optimize ProjectTeam#max_member_access performance. !4973
+ - Use update_columns to bypass all the dirty code on active_record. !4985
+ - Fix restore Rake task warning message output !4980
+
+v 8.9.2
+ - Fix visibility of snippets when searching.
+ - Fix an information disclosure when requesting access to a group containing private projects.
+ - Update omniauth-saml to 1.6.0 !4951
+
+v 8.9.1
+ - Refactor labels documentation. !3347
+ - Eager load award emoji on notes. !4628
+ - Fix some CI wording in documentation. !4660
+ - Document `GIT_STRATEGY` and `GIT_DEPTH`. !4720
+ - Add documentation for the export & import features. !4732
+ - Add some docs for Docker Registry configuration. !4738
+ - Ensure we don't send the "access request declined" email to access requesters on project deletion. !4744
+ - Display group/project access requesters separately in the admin area. !4798
+ - Add documentation and examples for configuring cloud storage for registry images. !4812
+ - Clarifies documentation about artifact expiry. !4831
+ - Fix the Network graph links. !4832
+ - Fix MR-auto-close text added to description. !4836
+ - Add documentation for award emoji now that comments can be awarded with emojis. !4839
+ - Fix typo in export failure email. !4847
+ - Fix header vertical centering. !4170
+ - Fix subsequent SAML sign ins. !4718
+ - Set button label when picking an option from status dropdown. !4771
+ - Prevent invalid URLs from raising exceptions in WikiLink Filter. !4775
+ - Handle external issues in IssueReferenceFilter. !4789
+ - Support for rendering/redacting multiple documents. !4828
+ - Update Todos documentation and screenshots to include new functionality. !4840
+ - Hide nav arrows by default. !4843
+ - Added bottom padding to label color suggestion link. !4845
+ - Use jQuery objects in ref dropdown. !4850
+ - Fix GitLab project import issues related to notes and builds. !4855
+ - Restrict header logo to 36px so it doesn't overflow. !4861
+ - Fix unwanted label unassignment. !4863
+ - Fix mobile Safari bug where horizontal nav arrows would flicker on scroll. !4869
+ - Restore old behavior around diff notes to outdated discussions. !4870
+ - Fix merge requests project settings help link anchor. !4873
+ - Fix 404 when accessing pipelines as guest user on public projects. !4881
+ - Remove width restriction for logo on sign-in page. !4888
+ - Bump gitlab_git to 10.2.3 to fix false truncated warnings with ISO-8559 files. !4884
+ - Apply selected value as label. !4886
+ - Fix temp file being deleted after the request while importing a GitLab project. !4894
+ - Fix pagination when sorting by columns with lots of ties (like priority)
+ - Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise.
+ - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
+ - Fix a wrong MR status when merge_when_build_succeeds & project.only_allow_merge_if_build_succeeds are true. !4912
+ - Add SMTP as default delivery method to match gitlab-org/omnibus-gitlab!826. !4915
+ - Remove duplicate 'New Page' button on edit wiki page
+
+v 8.9.0
+ - Fix group visibility form layout in application settings
+ - Fix builds API response not including commit data
+ - Fix error when CI job variables key specified but not defined
+ - Fix pipeline status when there are no builds in pipeline
+ - Fix Error 500 when using closes_issues API with an external issue tracker
+ - Add more information into RSS feed for issues (Alexander Matyushentsev)
+ - Bulk assign/unassign labels to issues.
+ - Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
+ - Show Star and Fork buttons on mobile.
+ - Performance improvements on RelativeLinkFilter
+ - Fix endless redirections when accessing user OAuth applications when they are disabled
+ - Allow enabling wiki page events from Webhook management UI
+ - Bump rouge to 1.11.0
+ - Fix issue with arrow keys not working in search autocomplete dropdown
+ - Fix an issue where note polling stopped working if a window was in the
+ background during a refresh.
+ - Pre-processing Markdown now only happens when needed
+ - Make EmailsOnPushWorker use Sidekiq mailers queue
+ - Redesign all Devise emails. !4297
+ - Don't show 'Leave Project' to group members
+ - Fix wiki page events' webhook to point to the wiki repository
+ - Add a border around images to differentiate them from the background.
+ - Don't show tags for revert and cherry-pick operations
+ - Show image ID on registry page
+ - Fix issue todo not remove when leave project !4150 (Long Nguyen)
+ - Allow customisable text on the 'nearly there' page after a user signs up
+ - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
+ - Fix SVG sanitizer to allow more elements
+ - Allow forking projects with restricted visibility level
+ - Added descriptions to notification settings dropdown
+ - Improve note validation to prevent errors when creating invalid note via API
+ - Reduce number of fog gem dependencies
+ - Add number of merge requests for a given milestone to the milestones view.
+ - Implement a fair usage of shared runners
+ - Remove project notification settings associated with deleted projects
+ - Fix 404 page when viewing TODOs that contain milestones or labels in different projects
+ - Add a metric for the number of new Redis connections created by a transaction
+ - Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
+ - Redesign navigation for project pages
+ - Fix images in sign-up confirmation email
+ - Added shortcut 'y' for copying a files content hash URL #14470
+ - Fix groups API to list only user's accessible projects
+ - Fix horizontal scrollbar for long commit message.
+ - GitLab Performance Monitoring now tracks the total method execution time and call count per method
+ - Add Environments and Deployments
+ - Redesign account and email confirmation emails
+ - Don't fail builds for projects that are deleted
+ - Support Docker Registry manifest v1
+ - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
+ - Bump nokogiri to 1.6.8
+ - Use gitlab-shell v3.0.0
+ - Fixed alignment of download dropdown in merge requests
+ - Upgrade to jQuery 2
+ - Adds selected branch name to the dropdown toggle
+ - Add API endpoint for Sidekiq Metrics !4653
+ - Refactoring Award Emoji with API support for Issues and MergeRequests
+ - Use Knapsack to evenly distribute tests across multiple nodes
+ - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
+ - Don't allow MRs to be merged when commits were added since the last review / page load
+ - Add DB index on users.state
+ - Limit email on push diff size to 30 files / 150 KB
+ - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
+ - Changed the Slack build message to use the singular duration if necessary (Aran Koning)
+ - Fix race condition on merge when build succeeds
+ - Links from a wiki page to other wiki pages should be rewritten as expected
+ - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
+ - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
+ - Fix issues filter when ordering by milestone
+ - Disable SAML account unlink feature
+ - Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
+ - Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
+ - TeamCity Service: Fix URL handling when base URL contains a path
+ - Todos will display target state if issuable target is 'Closed' or 'Merged'
+ - Validate only and except regexp
+ - Fix bug when sorting issues by milestone due date and filtering by two or more labels
+ - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
+ - Add support for using Yubikeys (U2F) for two-factor authentication
+ - Link to blank group icon doesn't throw a 404 anymore
+ - Remove 'main language' feature
+ - Toggle whitespace button now available for compare branches diffs #17881
+ - Pipelines can be canceled only when there are running builds
+ - Allow authentication using personal access tokens
+ - Use downcased path to container repository as this is expected path by Docker
+ - Allow to use CI token to fetch LFS objects
+ - Custom notification settings
+ - Projects pending deletion will render a 404 page
+ - Measure queue duration between gitlab-workhorse and Rails
+ - Added Gfm autocomplete for labels
+ - Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114
+ - Make Omniauth providers specs to not modify global configuration
+ - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
+ - Make authentication service for Container Registry to be compatible with < Docker 1.11
+ - Make it possible to lock a runner from being enabled for other projects
+ - Add Application Setting to configure Container Registry token expire delay (default 5min)
+ - Cache assigned issue and merge request counts in sidebar nav
+ - Use Knapsack only in CI environment
+ - Updated project creation page to match new UI #2542
+ - Cache project build count in sidebar nav
+ - Add milestone expire date to the right sidebar
+ - Manually mark a issue or merge request as a todo
+ - Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
+ - Reduce number of queries needed to render issue labels in the sidebar
+ - Improve error handling importing projects
+ - Remove duplicated notification settings
+ - Put project Files and Commits tabs under Code tab
+ - Decouple global notification level from user model
+ - Replace Colorize with Rainbow for coloring console output in Rake tasks.
+ - Add workhorse controller and API helpers
+ - An indicator is now displayed at the top of the comment field for confidential issues.
+ - Show categorised search queries in the search autocomplete
+ - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
+ - Dropdown for `.gitlab-ci.yml` templates
+ - Improve issuables APIs performance when accessing notes !4471
+ - Add sorting dropdown to tags page !4423
+ - External links now open in a new tab
+ - Prevent default actions of disabled buttons and links
+ - Markdown editor now correctly resets the input value on edit cancellation !4175
+ - Toggling a task list item in a issue/mr description does not creates a Todo for mentions
+ - Improved UX of date pickers on issue & milestone forms
+ - Cache on the database if a project has an active external issue tracker.
+ - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
+ - GitLab project import and export functionality
+ - All classes in the Banzai::ReferenceParser namespace are now instrumented
+ - Remove deprecated issues_tracker and issues_tracker_id from project model
+ - Allow users to create confidential issues in private projects
+ - Measure CPU time for instrumented methods
+ - Instrument private methods and private instance methods by default instead just public methods
+ - Only show notes through JSON on confidential issues that the user has access to
+ - Updated the allocations Gem to version 1.0.5
+ - The background sampler now ignores classes without names
+ - Update design for `Close` buttons
+ - New custom icons for navigation
+ - Horizontally scrolling navigation on project, group, and profile settings pages
+ - Hide global side navigation by default
+ - Fix project Star/Unstar project button tooltip
+ - Remove tanuki logo from side navigation; center on top nav
+ - Include user relationships when retrieving award_emoji
+ - Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
+ - Set inverse_of for Project/Service association to reduce the number of queries
+ - Update tanuki logo highlight/loading colors
+ - Remove explicit Gitlab::Metrics.action assignments, are already automatic.
+ - Use Git cached counters for branches and tags on project page
+ - Cache participable participants in an instance variable.
+ - Filter parameters for request_uri value on instrumented transactions.
+ - Remove duplicated keys add UNIQUE index to keys fingerprint column
+ - ExtractsPath get ref_names from repository cache, if not there access git.
+ - Show a flash warning about the error detail of XHR requests which failed with status code 404 and 500
+ - Cache user todo counts from TodoService
+ - Ensure Todos counters doesn't count Todos for projects pending delete
+ - Add left/right arrows horizontal navigation
+ - Add tooltip to pin/unpin navbar
+ - Add new sub nav style to Wiki and Graphs sub navigation
+
+v 8.8.7
+ - Fix privilege escalation issue with OAuth external users.
+ - Ensure references to private repos aren't shown to logged-out users.
+
+v 8.8.6
+ - Fix visibility of snippets when searching.
+ - Update omniauth-saml to 1.6.0 !4951
+
+v 8.8.5
+ - Import GitHub repositories respecting the API rate limit !4166
+ - Fix todos page throwing errors when you have a project pending deletion !4300
+ - Disable Webhooks before proceeding with the GitHub import !4470
+ - Fix importer for GitHub comments on diff !4488
+ - Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498
+ - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541
+ - Prevent unauthorized access for projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
+ - Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
+ - Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
+
+v 8.8.4
+ - Fix LDAP-based login for users with 2FA enabled. !4493
+ - Added descriptions to notification settings dropdown
+ - Due date can be removed from milestones
+
+v 8.8.3
+ - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
+ - Fixed JS error when trying to remove discussion form. !4303
+ - Fixed issue with button color when no CI enabled. !4287
+ - Fixed potential issue with 2 CI status polling events happening. !3869
+ - Improve design of Pipeline view. !4230
+ - Fix gitlab importer failing to import new projects due to missing credentials. !4301
+ - Fix import URL migration not rescuing with the correct Error. !4321
+ - Fix health check access token changing due to old application settings being used. !4332
+ - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363
+ - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364
+ - Pass the "Remember me" value to the 2FA token form. !4369
+ - Fix incorrect links on pipeline page when merge request created from fork. !4376
+ - Use downcased path to container repository as this is expected path by Docker. !4420
+ - Fix wiki project clone address error (chujinjin). !4429
+ - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392
+ - Fix missing number on generated ordered list element. !4437
+ - Prevent disclosure of notes on confidential issues in search results.
+
+v 8.8.2
+ - Added remove due date button. !4209
+ - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources. !4242
+ - Fix Error 500 in CI charts by gracefully handling commits with no durations. !4245
+ - Fix table UI on CI builds page. !4249
+ - Fix backups if registry is disabled. !4263
+ - Fixed issue with merge button color. !4211
+ - Fixed issue with enter key selecting wrong option in dropdown. !4210
+ - When creating a .gitignore file a dropdown with templates will be provided. !4075
+ - Fix concurrent request when updating build log in browser. !4183
+
+v 8.8.1
+ - Add documentation for the "Health Check" feature
+ - Allow anonymous users to access a public project's pipelines !4233
+ - Fix MySQL compatibility in zero downtime migrations helpers
+ - Fix the CI login to Container Registry (the gitlab-ci-token user)
+
+v 8.8.0
+ - Implement GFM references for milestones (Alejandro Rodríguez)
+ - Snippets tab under user profile. !4001 (Long Nguyen)
- Fix error when using link to uploads in global snippets
+ - Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref
- Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen)
- Use a case-insensitive comparison in sanitizing URI schemes
+ - Toggle sign-up confirmation emails in application settings
+ - Make it possible to prevent tagged runner from picking untagged jobs
+ - Added `InlineDiffFilter` to the markdown parser. (Adam Butler)
+ - Added inline diff styling for `change_title` system notes. (Adam Butler)
- Project#open_branches has been cleaned up and no longer loads entire records into memory.
- Escape HTML in commit titles in system note messages
+ - Improve design of Pipeline View
+ - Fix scope used when accessing container registry
+ - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios
- Improve multiple branch push performance by memoizing permission checking
- Log to application.log when an admin starts and stops impersonating a user
+ - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi)
- Updated gitlab_git to 10.1.0
- GitAccess#protected_tag? no longer loads all tags just to check if a single one exists
- Reduce delay in destroying a project from 1-minute to immediately
- Make build status canceled if any of the jobs was canceled and none failed
- Upgrade Sidekiq to 4.1.2
+ - Added /health_check endpoint for checking service status
+ - Make 'upcoming' filter for milestones work better across projects
- Sanitize repo paths in new project error message
- Bump mail_room to 0.7.0 to fix stuck IDLE connections
- Remove future dates from contribution calendar graph.
- Support e-mail notifications for comments on project snippets
+ - Fix API leak of notes of unauthorized issues, snippets and merge requests
- Use ActionDispatch Remote IP for Akismet checking
- Fix error when visiting commit builds page before build was updated
- Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project
- Update SVG sanitizer to conform to SVG 1.1
+ - Speed up push emails with multiple recipients by only generating the email once
- Updated search UI
+ - Added authentication service for Container Registry
- Display informative message when new milestone is created
- Sanitize milestones and labels titles
- Support multi-line tag messages. !3833 (Calin Seciu)
+ - Force users to reset their password after an admin changes it
- Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea)
- Added button to toggle whitespaces changes on diff view
- Backport GitHub Enterprise import support from EE
- Create tags using Rugged for performance reasons. !3745
+ - Allow guests to set notification level in projects
- API: Expose Issue#user_notes_count. !3126 (Anton Popov)
+ - Don't show forks button when user can't view forks
+ - Fix atom feed links and rendering
- Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718
- Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes)
- Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724
- Added multiple colors for labels in dropdowns when dups happen.
+ - Show commits in the same order as `git log`
- Improve description for the Two-factor Authentication sign-in screen. (Connor Shea)
- API support for the 'since' and 'until' operators on commit requests (Paco Guzman)
- Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko)
- Expire repository exists? and has_visible_content? caches after a push if necessary
- - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi)
+ - Fix unintentional filtering bug in Issue/MR sorted by milestone due (Takuya Noguchi)
- Fix adding a todo for private group members (Ahmad Sherif)
- Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3
+ - Total method execution timings are no longer tracked
+ - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga)
+ - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif)
+ - Hide left sidebar on phone screens to give more space for content
+ - Redesign navigation for profile and group pages
+ - Add counter metrics for rails cache
+ - Import pull requests from GitHub where the source or target branches were removed
+ - All Grape API helpers are now instrumented
+ - Improve Issue formatting for the Slack Service (Jeroen van Baarsen)
+ - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
+ - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs)
+ - When creating a .gitignore file a dropdown with templates will be provided
+ - Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562
+
+v 8.7.9
+ - Fix privilege escalation issue with OAuth external users.
+ - Ensure references to private repos aren't shown to logged-out users.
+
+v 8.7.8
+ - Fix visibility of snippets when searching.
+ - Update omniauth-saml to 1.6.0 !4951
+
+v 8.7.7
+ - Fix import by `Any Git URL` broken if the URL contains a space
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
+
+v 8.7.6
+ - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
+ - Fix import from GitLab.com to a private instance failure. !4181
+ - Fix external imports not finding the import data. !4106
+ - Fix notification delay when changing status of an issue
+ - Bump Workhorse to 0.7.5 so it can serve raw diffs
+
+v 8.7.5
+ - Fix relative links in wiki pages. !4050
+ - Fix always showing build notification message when switching between merge requests !4086
+ - Fix an issue when filtering merge requests with more than one label. !3886
+ - Fix short note for the default scope on build page (Takuya Noguchi)
v 8.7.4
- - Fix always showing build notification message when switching between merge requests
- - Links for Redmine issue references are generated correctly again (Benedikt Huss)
- - Fix an issue when filtering merge requests with more than one label. !3886
+ - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss)
+ - Fix setting trusted proxies !3970
+ - Fix BitBucket importer bug when throwing exceptions !3941
+ - Use sign out path only if not empty !3989
+ - Running rake gitlab:db:drop_tables now drops tables with cascade !4020
+ - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100
+ - Use a case-insensitive comparison in sanitizing URI schemes
v 8.7.3
- Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented
- Merge request widget displays TeamCity build state and code coverage correctly again.
- Fix the line code when importing PR review comments from GitHub. !4010
- Wikis are now initialized on legacy projects when checking repositories
+ - Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea)
v 8.7.2
- The "New Branch" button is now loaded asynchronously
@@ -185,6 +619,11 @@ v 8.7.0
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
+v 8.6.9
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
+
v 8.6.8
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -339,6 +778,10 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
+v 8.5.13
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.5.12
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -500,6 +943,10 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.11
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.4.10
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -636,6 +1083,10 @@ v 8.4.0
- 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.10
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.3.9
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -754,6 +1205,10 @@ v 8.3.0
- 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.6
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.2.5
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -860,7 +1315,7 @@ v 8.1.3
- Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication
-v 8.1.2
+v 8.1.1
- Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory
- Fix specific runners visibility
@@ -1485,20 +1940,17 @@ v 7.10.0
- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
- Fix merge request comments on files with multiple commits
- Fix Resource Owner Password Authentication Flow
-
-v 7.9.4
- - Security: Fix project import URL regex to prevent arbitary local repos from being imported
- - Fixed issue where only 25 commits would load in file listings
- - Fix LDAP identities after config update
-
-v 7.9.3
- - Contains no changes
- Add icons to Add dropdown items.
- Allow admin to create public deploy keys that are accessible to any project.
- Warn when gitlab-shell version doesn't match requirement.
- Skip email confirmation when set by admin or via LDAP.
- Only allow users to reference groups, projects, issues, MRs, commits they have access to.
+v 7.9.4
+ - Security: Fix project import URL regex to prevent arbitary local repos from being imported
+ - Fixed issue where only 25 commits would load in file listings
+ - Fix LDAP identities after config update
+
v 7.9.3
- Contains no changes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9fe4cf7b0f6..f4472214778 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -96,7 +96,7 @@ 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].
+The current designs can be found in the [`gitlab8.atype` file].
### UI development kit
@@ -308,16 +308,14 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
-1. Create a feature branch
+1. Create a feature branch, branch away from `master`.
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
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 are writing documentation, 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. 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, see the [merge request description format]
@@ -407,6 +405,7 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
+1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases
@@ -532,4 +531,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[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/
+[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
+[license-finder-doc]: doc/development/licensing.md
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 37c2961c243..944880fa15e 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.7.2
+3.2.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 39e898a4f95..879be8a98fc 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.1
+0.7.7
diff --git a/Gemfile b/Gemfile
index 77d351419f0..f47084f9d90 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,9 +18,8 @@ gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
-gem 'devise', '~> 3.5.4'
-gem 'doorkeeper', '~> 3.1'
-gem 'devise-async', '~> 0.9.0'
+gem 'devise', '~> 4.0'
+gem 'doorkeeper', '~> 4.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
@@ -31,27 +30,29 @@ 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.5.0'
+gem 'omniauth-saml', '~> 1.6.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'rack-oauth2', '~> 1.2.1'
+gem 'jwt'
# Spam and anti-bot protection
-gem 'recaptcha', require: 'recaptcha/rails'
+gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0'
# Two-factor authentication
-gem 'devise-two-factor', '~> 2.0.0'
+gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
-gem 'attr_encrypted', '~> 1.3.4'
+gem 'attr_encrypted', '~> 3.0.0'
+gem 'u2f', '~> 0.2.1'
# Browser detection
-gem "browser", '~> 1.0.0'
+gem "browser", '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 10.0'
+gem "gitlab_git", '~> 10.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -72,10 +73,10 @@ gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
-gem "kaminari", "~> 0.16.3"
+gem "kaminari", "~> 0.17.0"
# HAML
-gem "haml-rails", '~> 0.9.0'
+gem 'hamlit', '~> 2.5'
# Files attachments
gem "carrierwave", '~> 0.10.0'
@@ -83,8 +84,16 @@ gem "carrierwave", '~> 0.10.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
+# for backups
+gem 'fog-aws', '~> 0.9'
+gem 'fog-azure', '~> 0.0'
+gem 'fog-core', '~> 1.40'
+gem 'fog-local', '~> 0.3'
+gem 'fog-google', '~> 0.3'
+gem 'fog-openstack', '~> 0.1'
+gem 'fog-rackspace', '~> 0.1.1'
+
# for aws storage
-gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4'
# Authorization
@@ -98,13 +107,13 @@ gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'github-markup', '~> 1.3.1'
gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.2.9'
+gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 1.10.1'
+gem 'rouge', '~> 1.11'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -120,7 +129,7 @@ group :unicorn do
end
# State machine
-gem "state_machines-activerecord", '~> 0.3.0'
+gem "state_machines-activerecord", '~> 0.4.0'
# Run events after state machine commits
gem 'after_commit_queue'
@@ -137,7 +146,7 @@ gem 'redis-namespace'
gem "httparty", '~> 0.13.3'
# Colored output to console
-gem "colorize", '~> 0.7.0'
+gem "rainbow", '~> 2.1.0'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -177,9 +186,6 @@ gem 'ruby-fogbugz', '~> 0.2.1'
# d3
gem 'd3_rails', '~> 3.5.0'
-#cal-heatmap
-gem 'cal-heatmap-rails', '~> 3.6.0'
-
# underscore-rails
gem "underscore-rails", "~> 1.8.0"
@@ -205,6 +211,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
+# Parse duration
+gem 'chronic_duration', '~> 0.10.6'
+
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
@@ -213,21 +222,20 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
-gem 'font-awesome-rails', '~> 4.2'
-gem 'gitlab_emoji', '~> 0.3.0'
+gem 'font-awesome-rails', '~> 4.6.1'
+gem 'gemojione', '~> 2.6'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
-gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
+gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 0.15'
+gem 'sentry-raven', '~> 1.1.0'
gem 'premailer-rails', '~> 1.9.0'
@@ -240,10 +248,9 @@ end
group :development do
gem "foreman"
- gem 'brakeman', '~> 3.2.0', require: false
+ gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
- gem 'quiet_assets', '~> 1.0.2'
gem 'rerun', '~> 0.11.0'
gem 'bullet', require: false
gem 'rblineprof', platform: :mri, require: false
@@ -257,7 +264,7 @@ group :development do
gem "sdoc", '~> 0.3.20'
# thin instead webrick
- gem 'thin', '~> 1.6.1'
+ gem 'thin', '~> 1.7.0'
end
group :development, :test do
@@ -269,7 +276,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails', '~> 4.6.0'
- gem 'rspec-rails', '~> 3.4.0'
+ gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
@@ -292,15 +299,18 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.38.0', require: false
+ gem 'rubocop', '~> 0.40.0', require: false
+ gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
- gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.11.0', require: false
gem 'flog', require: false
gem 'flay', require: false
gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false
+
+ gem "license_finder", require: false
+ gem 'knapsack'
end
group :test do
@@ -319,17 +329,23 @@ gem "newrelic_rpm", '~> 3.14'
gem 'octokit', '~> 4.3.0'
-gem "mail_room", "~> 0.7"
+gem "mail_room", "~> 0.8"
gem 'email_reply_parser', '~> 0.5.8'
## CI
-gem 'activerecord-deprecated_finders', '~> 1.0.3'
-gem 'activerecord-session_store', '~> 0.1.0'
+gem 'activerecord-session_store', '~> 1.0.0'
gem "nested_form", '~> 0.3.2'
# OAuth
-gem 'oauth2', '~> 1.0.0'
+gem 'oauth2', '~> 1.2.0'
# Soft deletion
gem "paranoia", "~> 2.0"
+
+# Health check
+gem 'health_check', '~> 1.5.1'
+
+# System information
+gem 'vmstat', '~> 2.1.0'
+gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index c02698bcca7..055596b056f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,8 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (2.3.2)
- RedCloth (4.2.9)
+ RedCloth (4.3.2)
ace-rails-ap (4.0.2)
actionmailer (4.2.6)
actionpack (= 4.2.6)
@@ -33,11 +32,12 @@ GEM
activemodel (= 4.2.6)
activesupport (= 4.2.6)
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)
+ activerecord-session_store (1.0.0)
+ actionpack (>= 4.0, < 5.1)
+ activerecord (>= 4.0, < 5.1)
+ multi_json (~> 1.11, >= 1.11.2)
+ rack (>= 1.5.2, < 3)
+ railties (>= 4.0, < 5.1)
activesupport (4.2.6)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
- allocations (1.0.4)
+ allocations (1.0.5)
arel (6.0.3)
asana (0.4.0)
faraday (~> 0.9)
@@ -59,8 +59,8 @@ GEM
oauth2 (~> 1.0)
asciidoctor (1.5.3)
ast (2.2.0)
- attr_encrypted (1.3.4)
- encryptor (>= 1.3.0)
+ attr_encrypted (3.0.1)
+ encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
execjs
@@ -70,8 +70,24 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
+ azure (0.7.5)
+ addressable (~> 2.3)
+ azure-core (~> 0.1)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.10)
+ json (~> 1.8)
+ mime-types (>= 1, < 3.0)
+ nokogiri (~> 1.6)
+ systemu (~> 2.6)
+ thor (~> 0.19)
+ uuid (~> 2.0)
+ azure-core (0.1.2)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.10)
+ nokogiri (~> 1.6)
babosa (1.0.2)
- bcrypt (3.1.10)
+ base32 (0.3.2)
+ bcrypt (3.1.11)
benchmark-ips (2.3.0)
better_errors (1.0.1)
coderay (>= 1.0.0)
@@ -81,17 +97,8 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.2.1)
- erubis (~> 2.6)
- haml (>= 3.0, < 5.0)
- highline (>= 1.6.20, < 2.0)
- ruby2ruby (~> 2.3.0)
- ruby_parser (~> 3.8.1)
- safe_yaml (>= 1.0)
- sass (~> 3.0)
- slim (>= 1.3.6, < 4.0)
- terminal-table (~> 1.4)
- browser (1.0.1)
+ brakeman (3.3.2)
+ browser (2.2.0)
builder (3.2.2)
bullet (5.0.0)
activesupport (>= 3.0.0)
@@ -100,7 +107,6 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.1)
- cal-heatmap-rails (3.6.0)
capybara (2.6.2)
addressable
mime-types (>= 1.16)
@@ -118,6 +124,8 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
+ chronic_duration (0.10.6)
+ numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
@@ -133,12 +141,6 @@ GEM
colorize (0.7.7)
concurrent-ruby (1.0.2)
connection_pool (2.2.0)
- coveralls (0.8.13)
- json (~> 1.8)
- simplecov (~> 0.11.0)
- term-ansicolor (~> 1.3)
- thor (~> 0.19.1)
- tins (~> 1.6.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
@@ -154,38 +156,35 @@ GEM
activerecord (>= 3.2.0, < 5.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (3.5.4)
+ devise (4.1.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 3.2.6, < 5)
+ railties (>= 4.1.0, < 5.1)
responders
- thread_safe (~> 0.1)
warden (~> 1.2.3)
- devise-async (0.9.0)
- devise (~> 3.2)
- devise-two-factor (2.0.1)
+ devise-two-factor (3.0.0)
activesupport
- attr_encrypted (~> 1.3.2)
- devise (~> 3.5.0)
+ attr_encrypted (>= 1.3, < 4, != 2)
+ devise (~> 4.0)
railties
- rotp (~> 2)
+ rotp (~> 2.0)
diff-lcs (1.2.5)
diffy (3.0.7)
docile (1.1.5)
- doorkeeper (3.1.0)
- railties (>= 3.2)
+ doorkeeper (4.0.0)
+ railties (>= 4.2)
dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_parser (0.5.8)
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
- encryptor (1.3.0)
+ encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
- excon (0.45.4)
+ excon (0.49.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_girl (4.5.0)
@@ -202,8 +201,6 @@ GEM
multi_json
ffaker (2.0.0)
ffi (1.9.10)
- fission (0.5.0)
- CFPropertyList (~> 2.2)
flay (2.6.1)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
@@ -213,113 +210,42 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog (1.36.0)
- fog-aliyun (>= 0.1.0)
- fog-atmos
- fog-aws (>= 0.6.0)
- fog-brightbox (~> 0.4)
- 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)
- fog-aliyun (0.1.0)
+ fog-aws (0.9.2)
fog-core (~> 1.27)
fog-json (~> 1.0)
+ fog-xml (~> 0.1)
ipaddress (~> 0.8)
- xml-simple (~> 1.1)
- fog-atmos (0.1.0)
- fog-core
- fog-xml
- fog-aws (0.8.1)
+ fog-azure (0.0.2)
+ azure (~> 0.6)
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
- inflecto (~> 0.0.2)
- fog-core (1.35.0)
+ fog-core (1.40.0)
builder
- excon (~> 0.45)
+ excon (~> 0.49)
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-google (0.3.2)
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-local (0.3.0)
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.5)
- fog-core (>= 1.21.0)
- fog-json
- fog-xml (>= 0.0.1)
- 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.3)
- fog-core
- fog-json
- fog-storm_on_demand (0.1.1)
- fog-core
- fog-json
- fog-terremark (0.1.0)
- fog-core
- fog-xml
- fog-vmfusion (0.1.0)
- fission
- fog-core
- fog-voxel (0.1.0)
- fog-core
- fog-xml
- fog-xenserver (0.2.2)
- fog-core
- fog-xml
+ fog-openstack (0.1.6)
+ fog-core (>= 1.39)
+ fog-json (>= 1.0)
+ ipaddress (>= 0.8)
+ fog-rackspace (0.1.1)
+ fog-core (>= 1.35)
+ fog-json (>= 1.0)
+ fog-xml (>= 0.1)
+ ipaddress (>= 0.8)
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.5.0.1)
+ font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
thor (~> 0.19.1)
@@ -329,7 +255,7 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.2.1)
+ gemojione (2.6.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
@@ -348,9 +274,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_emoji (0.3.1)
- gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (10.1.0)
+ gitlab_git (10.2.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -393,27 +317,19 @@ GEM
grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
- haml (4.0.7)
+ hamlit (2.5.0)
+ temple (~> 0.7.6)
+ thor
tilt
- haml-rails (0.9.0)
- actionpack (>= 4.0.1)
- activesupport (>= 4.0.1)
- haml (>= 4.0.6, < 5.0)
- html2haml (>= 1.0.1)
- railties (>= 4.0.1)
hashie (3.4.3)
- highline (1.7.8)
+ health_check (1.5.1)
+ rails (>= 2.3.0)
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
- html2haml (2.0.0)
- erubis (~> 2.7.0)
- haml (~> 4.0.0)
- nokogiri (~> 1.6.0)
- ruby_parser (~> 3.5)
htmlentities (4.3.4)
http_parser.rb (0.5.3)
httparty (0.13.7)
@@ -422,29 +338,29 @@ GEM
httpclient (2.7.0.1)
i18n (0.7.0)
ice_nine (0.11.1)
- inflecto (0.0.2)
influxdb (0.2.3)
cause
json
- ipaddress (0.8.2)
+ ipaddress (0.8.3)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- jquery-scrollto-rails (1.4.3)
- railties (> 3.1, < 5.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
- jwt (1.5.2)
- kaminari (0.16.3)
+ jwt (1.5.4)
+ kaminari (0.17.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
kgio (2.10.0)
+ knapsack (1.11.0)
+ rake
+ timecop (>= 0.1.0)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -453,6 +369,12 @@ GEM
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
+ license_finder (2.1.0)
+ bundler
+ httparty
+ rubyzip
+ thor
+ xml-simple
licensee (8.0.0)
rugged (>= 0.24b)
listen (3.0.5)
@@ -464,14 +386,14 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.7.0)
+ mail_room (0.8.0)
method_source (0.8.2)
- mime-types (2.99.1)
+ mime-types (2.99.2)
mimemagic (0.3.0)
- mini_portile2 (2.0.0)
+ mini_portile2 (2.1.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
- multi_json (1.11.2)
+ multi_json (1.12.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
mysql2 (0.3.20)
@@ -479,15 +401,17 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
newrelic_rpm (3.14.1.311)
- nokogiri (1.6.7.2)
- mini_portile2 (~> 2.0.0.rc2)
+ nokogiri (1.6.8)
+ mini_portile2 (~> 2.1.0)
+ pkg-config (~> 1.1.7)
+ numerizer (0.1.1)
oauth (0.4.7)
- oauth2 (1.0.0)
+ oauth2 (1.2.0)
faraday (>= 0.8, < 0.10)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
- rack (~> 1.2)
+ rack (>= 1.2, < 3)
octokit (4.3.0)
sawyer (~> 0.7.0, >= 0.5.3)
omniauth (1.3.1)
@@ -532,9 +456,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
- omniauth-saml (1.5.0)
+ omniauth-saml (1.6.0)
omniauth (~> 1.3)
- ruby-saml (~> 1.1, >= 1.1.1)
+ ruby-saml (~> 1.3)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
@@ -549,9 +473,10 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.3.0.6)
+ parser (2.3.1.0)
ast (~> 2.2)
pg (0.18.4)
+ pkg-config (1.1.7)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -572,8 +497,6 @@ GEM
pry-rails (0.3.4)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
- quiet_assets (1.0.3)
- railties (>= 3.1, < 5.0)
rack (1.6.4)
rack-accept (0.4.5)
rack (>= 0.4)
@@ -619,7 +542,6 @@ GEM
rainbow (2.1.0)
raindrops (0.15.0)
rake (10.5.0)
- raphael-rails (2.1.2)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@@ -627,10 +549,10 @@ GEM
debugger-ruby_core_source (~> 1.3)
rdoc (3.12.2)
json (~> 1.4)
- recaptcha (1.0.2)
+ recaptcha (3.0.0)
json
redcarpet (3.3.3)
- redis (3.3.0)
+ redis (3.2.2)
redis-actionpack (4.0.1)
actionpack (~> 4)
redis-rack (~> 1.5.0)
@@ -654,63 +576,62 @@ GEM
listen (~> 3.0)
responders (2.1.1)
railties (>= 4.2.0, < 5.1)
- rinku (1.7.3)
- rotp (2.1.1)
- rouge (1.10.1)
+ rinku (2.0.0)
+ rotp (2.1.2)
+ rouge (1.11.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
- rspec (3.4.0)
- rspec-core (~> 3.4.0)
- rspec-expectations (~> 3.4.0)
- rspec-mocks (~> 3.4.0)
- rspec-core (3.4.4)
- rspec-support (~> 3.4.0)
- rspec-expectations (3.4.0)
+ rspec (3.5.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-core (3.5.0)
+ rspec-support (~> 3.5.0)
+ rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.4.0)
- rspec-mocks (3.4.1)
+ rspec-support (~> 3.5.0)
+ rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.4.0)
- rspec-rails (3.4.2)
- actionpack (>= 3.0, < 4.3)
- activesupport (>= 3.0, < 4.3)
- railties (>= 3.0, < 4.3)
- rspec-core (~> 3.4.0)
- rspec-expectations (~> 3.4.0)
- rspec-mocks (~> 3.4.0)
- rspec-support (~> 3.4.0)
+ rspec-support (~> 3.5.0)
+ rspec-rails (3.5.0)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ railties (>= 3.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-support (~> 3.5.0)
rspec-retry (0.4.5)
rspec-core
- rspec-support (3.4.1)
- rubocop (0.38.0)
- parser (>= 2.3.0.6, < 3.0)
+ rspec-support (3.5.0)
+ rubocop (0.40.0)
+ parser (>= 2.3.1.0, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
+ rubocop-rspec (1.5.0)
+ rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
- ruby-progressbar (1.7.5)
- ruby-saml (1.1.2)
+ ruby-progressbar (1.8.1)
+ ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
- uuid (~> 2.3)
- ruby2ruby (2.3.0)
- ruby_parser (~> 3.1)
- sexp_processor (~> 4.0)
- ruby_parser (3.8.1)
+ ruby_parser (3.8.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
+ rubyzip (1.2.0)
rufus-scheduler (3.1.10)
rugged (0.24.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
- sass (3.4.21)
- sass-rails (5.0.4)
- railties (>= 4.0.0, < 5.0)
+ sass (3.4.22)
+ sass-rails (5.0.5)
+ railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
@@ -724,12 +645,12 @@ GEM
sdoc (0.3.20)
json (>= 1.1.3)
rdoc (~> 3.10)
- seed-fu (2.3.5)
- activerecord (>= 3.1, < 4.3)
- activesupport (>= 3.1, < 4.3)
+ seed-fu (2.3.6)
+ activerecord (>= 3.1)
+ activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (0.15.6)
+ sentry-raven (1.1.0)
faraday (>= 0.7.6)
settingslogic (2.0.9)
sexp_processor (4.7.0)
@@ -737,10 +658,11 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.1.2)
+ sidekiq (4.1.4)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
redis (~> 3.2, >= 3.2.1)
+ sinatra (>= 1.4.7)
sidekiq-cron (0.4.0)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
@@ -751,15 +673,12 @@ GEM
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- sinatra (1.4.6)
- rack (~> 1.4)
+ sinatra (1.4.7)
+ rack (~> 1.5)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
- slim (3.0.6)
- temple (~> 0.7.3)
- tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
@@ -771,28 +690,30 @@ GEM
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
- spring (1.7.1)
+ spring (1.7.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
- sprockets (3.6.0)
+ sprockets (3.6.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-rails (3.0.4)
+ sprockets-rails (3.1.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
state_machines (0.4.0)
- state_machines-activemodel (0.3.0)
- activemodel (~> 4.1)
+ state_machines-activemodel (0.4.0)
+ activemodel (>= 4.1, < 5.1)
state_machines (>= 0.4.0)
- state_machines-activerecord (0.3.0)
- activerecord (~> 4.1)
+ state_machines-activerecord (0.4.0)
+ activerecord (>= 4.1, < 5.1)
state_machines-activemodel (>= 0.3.0)
stringex (2.5.2)
+ sys-filesystem (1.1.6)
+ ffi
systemu (2.6.5)
task_list (1.0.2)
html-pipeline
@@ -800,19 +721,17 @@ GEM
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
- temple (0.7.6)
- term-ansicolor (1.3.2)
- tins (~> 1.0)
- terminal-table (1.5.2)
+ temple (0.7.7)
test_after_commit (0.4.2)
activerecord (>= 3.2)
- thin (1.6.4)
+ thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
- rack (~> 1.0)
+ rack (>= 1, < 3)
thor (0.19.1)
thread_safe (0.3.5)
- tilt (2.0.2)
+ tilt (2.0.5)
+ timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
eventmachine (~> 1.0)
@@ -823,7 +742,6 @@ GEM
mime-types
multi_json (~> 1.7)
twitter-stream (~> 0.1)
- tins (1.6.0)
turbolinks (2.5.3)
coffee-rails
twitter-stream (0.1.16)
@@ -832,6 +750,7 @@ GEM
simple_oauth (~> 0.1.4)
tzinfo (1.2.2)
thread_safe (~> 0.1)
+ u2f (0.2.1)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
@@ -839,7 +758,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.0.2)
+ unicode-display_width (1.0.5)
unicorn (4.9.0)
kgio (~> 2.6)
rack
@@ -856,7 +775,8 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- warden (1.2.4)
+ vmstat (2.1.0)
+ warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
activemodel (>= 4.0)
@@ -881,10 +801,9 @@ PLATFORMS
ruby
DEPENDENCIES
- RedCloth (~> 4.2.9)
+ RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.0.2)
- activerecord-deprecated_finders (~> 1.0.3)
- activerecord-session_store (~> 0.1.0)
+ activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
@@ -892,36 +811,34 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
- attr_encrypted (~> 1.3.4)
+ attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
+ base32 (~> 0.3.0)
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.2.0)
- browser (~> 1.0.0)
+ brakeman (~> 3.3.0)
+ browser (~> 2.2)
bullet
bundler-audit
byebug
- cal-heatmap-rails (~> 3.6.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
+ chronic_duration (~> 0.10.6)
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.0)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
- devise (~> 3.5.4)
- devise-async (~> 0.9.0)
- devise-two-factor (~> 2.0.0)
+ devise (~> 4.0)
+ devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3)
- doorkeeper (~> 3.1)
+ doorkeeper (~> 4.0)
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
@@ -929,16 +846,22 @@ DEPENDENCIES
ffaker (~> 2.0.0)
flay
flog
- fog (~> 1.36.0)
- font-awesome-rails (~> 4.2)
+ fog-aws (~> 0.9)
+ fog-azure (~> 0.0)
+ fog-core (~> 1.40)
+ fog-google (~> 0.3)
+ fog-local (~> 0.3)
+ fog-openstack (~> 0.1)
+ fog-rackspace (~> 0.1.1)
+ font-awesome-rails (~> 4.6.1)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
+ gemojione (~> 2.6)
github-linguist (~> 4.7.0)
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 10.0)
+ gitlab_git (~> 10.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
@@ -946,21 +869,24 @@ DEPENDENCIES
gon (~> 6.0.1)
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
- haml-rails (~> 0.9.0)
+ hamlit (~> 2.5)
+ health_check (~> 1.5.1)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
influxdb (~> 0.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
- jquery-scrollto-rails (~> 1.4.3)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
- kaminari (~> 0.16.3)
+ jwt
+ kaminari (~> 0.17.0)
+ knapsack
letter_opener_web (~> 1.3.0)
+ license_finder
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.7)
+ mail_room (~> 0.8)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -969,7 +895,7 @@ DEPENDENCIES
net-ssh (~> 3.0.1)
newrelic_rpm (~> 3.14)
nokogiri (~> 1.6.7, >= 1.6.7.2)
- oauth2 (~> 1.0.0)
+ oauth2 (~> 1.2.0)
octokit (~> 4.3.0)
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
@@ -981,7 +907,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
- omniauth-saml (~> 1.5.0)
+ omniauth-saml (~> 1.6.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
@@ -991,16 +917,15 @@ DEPENDENCIES
poltergeist (~> 1.9.0)
premailer-rails (~> 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.6)
rails-deprecated_sanitizer (~> 1.0.3)
- raphael-rails (~> 2.1.2)
+ rainbow (~> 2.1.0)
rblineprof
rdoc (~> 3.6)
- recaptcha
+ recaptcha (~> 3.0)
redcarpet (~> 3.3.3)
redis (~> 3.2)
redis-namespace
@@ -1008,11 +933,12 @@ DEPENDENCIES
request_store (~> 1.3.0)
rerun (~> 0.11.0)
responders (~> 2.0)
- rouge (~> 1.10.1)
+ rouge (~> 1.11)
rqrcode-rails3 (~> 0.1.7)
- rspec-rails (~> 3.4.0)
+ rspec-rails (~> 3.5.0)
rspec-retry
- rubocop (~> 0.38.0)
+ rubocop (~> 0.40.0)
+ rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
@@ -1020,7 +946,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 0.15)
+ sentry-raven (~> 1.1.0)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
@@ -1037,14 +963,16 @@ DEPENDENCIES
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.6.0)
- state_machines-activerecord (~> 0.3.0)
+ state_machines-activerecord (~> 0.4.0)
+ sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
- thin (~> 1.6.1)
+ thin (~> 1.7.0)
tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
+ u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
@@ -1052,9 +980,10 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0)
virtus (~> 1.0.1)
+ vmstat (~> 2.1.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.12.3
+ 1.12.5
diff --git a/README.md b/README.md
index 418d06a45a5..fee93d5f9c3 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,7 @@
# GitLab
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master)
## Canonical source
diff --git a/Rakefile b/Rakefile
index 5dd389d5678..85fff2d51eb 100755
--- a/Rakefile
+++ b/Rakefile
@@ -8,3 +8,5 @@ relative_url_conf = File.expand_path('../config/initializers/relative_url', __FI
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks
+
+Knapsack.load_tasks if defined?(Knapsack)
diff --git a/VERSION b/VERSION
index d5a967c3933..213504430f3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.8.0-pre
+8.10.0-pre
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
index a82c751e001..85de7793440 100644
--- a/app/assets/images/auth_buttons/azure_64.png
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png
index 4b90a57bc7d..b3d022a5a70 100644
--- a/app/assets/images/auth_buttons/bitbucket_64.png
+++ b/app/assets/images/auth_buttons/bitbucket_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/facebook_64.png b/app/assets/images/auth_buttons/facebook_64.png
index 1f1a80d7368..71ffb1c6a1f 100644
--- a/app/assets/images/auth_buttons/facebook_64.png
+++ b/app/assets/images/auth_buttons/facebook_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/github_64.png b/app/assets/images/auth_buttons/github_64.png
index 182a1a3f734..1fa19c55d2f 100644
--- a/app/assets/images/auth_buttons/github_64.png
+++ b/app/assets/images/auth_buttons/github_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/gitlab_64.png b/app/assets/images/auth_buttons/gitlab_64.png
index 99a40583b3a..f675678dc9d 100644
--- a/app/assets/images/auth_buttons/gitlab_64.png
+++ b/app/assets/images/auth_buttons/gitlab_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png
index fb64f8bee68..720824230a5 100644
--- a/app/assets/images/auth_buttons/google_64.png
+++ b/app/assets/images/auth_buttons/google_64.png
Binary files differ
diff --git a/app/assets/images/auth_buttons/twitter_64.png b/app/assets/images/auth_buttons/twitter_64.png
index e3bd9169a34..a4f14de57ae 100644
--- a/app/assets/images/auth_buttons/twitter_64.png
+++ b/app/assets/images/auth_buttons/twitter_64.png
Binary files differ
diff --git a/app/assets/images/bg_fallback.png b/app/assets/images/bg_fallback.png
index e5fe659ba63..5c55bc79dec 100644
--- a/app/assets/images/bg_fallback.png
+++ b/app/assets/images/bg_fallback.png
Binary files differ
diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg
deleted file mode 100644
index 0e05674e840..00000000000
--- a/app/assets/images/ci/arch.jpg
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico
deleted file mode 100644
index 9663d4d00b9..00000000000
--- a/app/assets/images/ci/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif
deleted file mode 100644
index 2fcb8f2da0d..00000000000
--- a/app/assets/images/ci/loader.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png
deleted file mode 100644
index 752d26adba7..00000000000
--- a/app/assets/images/ci/no_avatar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png
deleted file mode 100644
index d5edc04e65f..00000000000
--- a/app/assets/images/ci/rails.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png
deleted file mode 100644
index 65d29e3fd89..00000000000
--- a/app/assets/images/ci/service_sample.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/dark-scheme-preview.png b/app/assets/images/dark-scheme-preview.png
index 2ef58e52549..8855babf147 100644
--- a/app/assets/images/dark-scheme-preview.png
+++ b/app/assets/images/dark-scheme-preview.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 1e7cf79ea45..6bacb0e92b6 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
index 74d67f7520d..99588b56616 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png
index 0c157546b9c..ca30b459019 100644
--- a/app/assets/images/gitlab_logo.png
+++ b/app/assets/images/gitlab_logo.png
Binary files differ
diff --git a/app/assets/images/gitorious-logo-black.png b/app/assets/images/gitorious-logo-black.png
index 78f17a9af79..4a55fdc225a 100644
--- a/app/assets/images/gitorious-logo-black.png
+++ b/app/assets/images/gitorious-logo-black.png
Binary files differ
diff --git a/app/assets/images/gitorious-logo-blue.png b/app/assets/images/gitorious-logo-blue.png
index 4962cffba31..5eaa327d3df 100644
--- a/app/assets/images/gitorious-logo-blue.png
+++ b/app/assets/images/gitorious-logo-blue.png
Binary files differ
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
index 7d89da97e11..5b55e12571c 100644
--- a/app/assets/images/icon-link.png
+++ b/app/assets/images/icon-link.png
Binary files differ
diff --git a/app/assets/images/images.png b/app/assets/images/images.png
index ad146246caf..bd60de994c4 100644
--- a/app/assets/images/images.png
+++ b/app/assets/images/images.png
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.png b/app/assets/images/mailers/gitlab_header_logo.png
new file mode 100644
index 00000000000..35ca1860887
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_header_logo.png
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_tanuki_2x.png b/app/assets/images/mailers/gitlab_tanuki_2x.png
new file mode 100644
index 00000000000..551dd6ce2ce
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_tanuki_2x.png
Binary files differ
diff --git a/app/assets/images/monokai-scheme-preview.png b/app/assets/images/monokai-scheme-preview.png
index fbb339c6a91..d9c7d2d8041 100644
--- a/app/assets/images/monokai-scheme-preview.png
+++ b/app/assets/images/monokai-scheme-preview.png
Binary files differ
diff --git a/app/assets/images/msapplication-tile.png b/app/assets/images/msapplication-tile.png
index 58bbf2b20cb..1e0e2ed73ce 100644
--- a/app/assets/images/msapplication-tile.png
+++ b/app/assets/images/msapplication-tile.png
Binary files differ
diff --git a/app/assets/images/no_avatar.png b/app/assets/images/no_avatar.png
index 8287acbce13..5383d687b53 100644
--- a/app/assets/images/no_avatar.png
+++ b/app/assets/images/no_avatar.png
Binary files differ
diff --git a/app/assets/images/no_group_avatar.png b/app/assets/images/no_group_avatar.png
index bfb31bb2184..71612d55286 100644
--- a/app/assets/images/no_group_avatar.png
+++ b/app/assets/images/no_group_avatar.png
Binary files differ
diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png
index 884378ec96a..52ad11ab7a1 100644
--- a/app/assets/images/slider_handles.png
+++ b/app/assets/images/slider_handles.png
Binary files differ
diff --git a/app/assets/images/touch-icon-ipad-retina.png b/app/assets/images/touch-icon-ipad-retina.png
index feb32b48ec9..516dc2f4710 100644
--- a/app/assets/images/touch-icon-ipad-retina.png
+++ b/app/assets/images/touch-icon-ipad-retina.png
Binary files differ
diff --git a/app/assets/images/touch-icon-ipad.png b/app/assets/images/touch-icon-ipad.png
index a6ddc543509..b2093d015b8 100644
--- a/app/assets/images/touch-icon-ipad.png
+++ b/app/assets/images/touch-icon-ipad.png
Binary files differ
diff --git a/app/assets/images/touch-icon-iphone-retina.png b/app/assets/images/touch-icon-iphone-retina.png
index 8bf7ccb7534..438654e0d20 100644
--- a/app/assets/images/touch-icon-iphone-retina.png
+++ b/app/assets/images/touch-icon-iphone-retina.png
Binary files differ
diff --git a/app/assets/images/touch-icon-iphone.png b/app/assets/images/touch-icon-iphone.png
index 87da550f8be..e5f87fbbcf6 100644
--- a/app/assets/images/touch-icon-iphone.png
+++ b/app/assets/images/touch-icon-iphone.png
Binary files differ
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee
new file mode 100644
index 00000000000..6d8faba40d7
--- /dev/null
+++ b/app/assets/javascripts/LabelManager.js.coffee
@@ -0,0 +1,92 @@
+class @LabelManager
+ errorMessage: 'Unable to update label prioritization at this time'
+
+ constructor: (opts = {}) ->
+ # Defaults
+ {
+ @togglePriorityButton = $('.js-toggle-priority')
+ @prioritizedLabels = $('.js-prioritized-labels')
+ @otherLabels = $('.js-other-labels')
+ } = opts
+
+ @prioritizedLabels.sortable(
+ items: 'li'
+ placeholder: 'list-placeholder'
+ axis: 'y'
+ update: @onPrioritySortUpdate.bind(@)
+ )
+
+ @bindEvents()
+
+ bindEvents: ->
+ @togglePriorityButton.on 'click', @, @onTogglePriorityClick
+
+ onTogglePriorityClick: (e) ->
+ e.preventDefault()
+ _this = e.data
+ $btn = $(e.currentTarget)
+ $label = $("##{$btn.data('domId')}")
+ action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
+
+ # Make sure tooltip will hide
+ $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
+ $tooltip.tooltip 'destroy'
+
+ _this.toggleLabelPriority($label, action)
+
+ toggleLabelPriority: ($label, action, persistState = true) ->
+ _this = @
+ url = $label.find('.js-toggle-priority').data 'url'
+
+ $target = @prioritizedLabels
+ $from = @otherLabels
+
+ # Optimistic update
+ if action is 'remove'
+ $target = @otherLabels
+ $from = @prioritizedLabels
+
+ if $from.find('li').length is 1
+ $from.find('.empty-message').removeClass('hidden')
+
+ if not $target.find('li').length
+ $target.find('.empty-message').addClass('hidden')
+
+ $label.detach().appendTo($target)
+
+ # Return if we are not persisting state
+ return unless persistState
+
+ if action is 'remove'
+ xhr = $.ajax url: url, type: 'DELETE'
+
+ # Restore empty message
+ $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
+ else
+ xhr = @savePrioritySort($label, action)
+
+ xhr.fail @rollbackLabelPosition.bind(@, $label, action)
+
+ onPrioritySortUpdate: ->
+ xhr = @savePrioritySort()
+
+ xhr.fail ->
+ new Flash(@errorMessage, 'alert')
+
+ savePrioritySort: () ->
+ $.post
+ url: @prioritizedLabels.data('url')
+ data:
+ label_ids: @getSortedLabelsIds()
+
+ rollbackLabelPosition: ($label, originalAction)->
+ action = if originalAction is 'remove' then 'add' else 'remove'
+ @toggleLabelPriority($label, action, false)
+
+ new Flash(@errorMessage, 'alert')
+
+ getSortedLabelsIds: ->
+ sortedIds = []
+ @prioritizedLabels.find('li').each ->
+ sortedIds.push $(@).data 'id'
+ sortedIds
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 5092e824e65..ed5a5d0260c 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,11 +1,14 @@
class @Activities
constructor: ->
- Pager.init 20, true
+ Pager.init 20, true, false, @updateTooltips
$(".event-filter-link").on "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
+ updateTooltips: ->
+ gl.utils.localTimeAgo($('.js-timeago', '#activity'))
+
reloadActivities: ->
$(".content_list").html ''
Pager.init 20, true
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index dd1bbb37551..cf46f15a156 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -1,14 +1,16 @@
@Api =
- groups_path: "/api/:version/groups.json"
- group_path: "/api/:version/groups/:id.json"
- 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"
- license_path: "/api/:version/licenses/:key"
+ groupsPath: "/api/:version/groups.json"
+ groupPath: "/api/:version/groups/:id.json"
+ namespacesPath: "/api/:version/namespaces.json"
+ groupProjectsPath: "/api/:version/groups/:id/projects.json"
+ projectsPath: "/api/:version/projects.json"
+ labelsPath: "/api/:version/projects/:id/labels"
+ licensePath: "/api/:version/licenses/:key"
+ gitignorePath: "/api/:version/gitignores/:key"
+ gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
group: (group_id, callback) ->
- url = Api.buildUrl(Api.group_path)
+ url = Api.buildUrl(Api.groupPath)
url = url.replace(':id', group_id)
$.ajax(
@@ -22,7 +24,7 @@
# Return groups list. Filtered by query
# Only active groups retrieved
groups: (query, skip_ldap, callback) ->
- url = Api.buildUrl(Api.groups_path)
+ url = Api.buildUrl(Api.groupsPath)
$.ajax(
url: url
@@ -36,7 +38,7 @@
# Return namespaces list. Filtered by query
namespaces: (query, callback) ->
- url = Api.buildUrl(Api.namespaces_path)
+ url = Api.buildUrl(Api.namespacesPath)
$.ajax(
url: url
@@ -50,7 +52,7 @@
# Return projects list. Filtered by query
projects: (query, order, callback) ->
- url = Api.buildUrl(Api.projects_path)
+ url = Api.buildUrl(Api.projectsPath)
$.ajax(
url: url
@@ -64,7 +66,7 @@
callback(projects)
newLabel: (project_id, data, callback) ->
- url = Api.buildUrl(Api.labels_path)
+ url = Api.buildUrl(Api.labelsPath)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
@@ -80,7 +82,7 @@
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
- url = Api.buildUrl(Api.group_projects_path)
+ url = Api.buildUrl(Api.groupProjectsPath)
url = url.replace(':id', group_id)
$.ajax(
@@ -95,7 +97,7 @@
# Return text for a specific license
licenseText: (key, data, callback) ->
- url = Api.buildUrl(Api.license_path).replace(':key', key)
+ url = Api.buildUrl(Api.licensePath).replace(':key', key)
$.ajax(
url: url
@@ -103,6 +105,18 @@
).done (license) ->
callback(license)
+ gitignoreText: (key, callback) ->
+ url = Api.buildUrl(Api.gitignorePath).replace(':key', key)
+
+ $.get url, (gitignore) ->
+ callback(gitignore)
+
+ gitlabCiYml: (key, callback) ->
+ url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
+
+ $.get url, (file) ->
+ callback(file)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index bffce5a0c0f..20fe5a5cc27 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
-#= require jquery
+#= require jquery2
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
#= require jquery-ui/draggable
@@ -18,8 +18,6 @@
#= require jquery.atwho
#= require jquery.scrollTo
#= require jquery.turbolinks
-#= require d3
-#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap/affix
@@ -34,11 +32,6 @@
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
-#= require raphael
-#= require g.raphael
-#= require g.bar
-#= require Chart
-#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
#= require underscore
@@ -52,9 +45,17 @@
#= require shortcuts_network
#= require jquery.nicescroll
#= require date.format
-#= require_tree .
+#= require_directory ./behaviors
+#= require_directory ./blob
+#= require_directory ./ci
+#= require_directory ./commit
+#= require_directory ./extensions
+#= require_directory ./lib/utils
+#= require_directory ./u2f
+#= require_directory .
#= require fuzzaldrin-plus
#= require cropper
+#= require u2f
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -120,9 +121,15 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+
+ $document = $(document)
+ $window = $(window)
+ $body = $('body')
+
+ gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
- $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
+ $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", ->
@@ -150,26 +157,13 @@ $ ->
), 1
# Initialize tooltips
- $('body').tooltip(
+ $body.tooltip(
selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
$el.data('placement') || 'bottom'
)
- $('.header-logo .home').tooltip(
- placement: (_, el) ->
- $el = $(el)
- if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
- container: 'body'
- )
-
- $('.page-with-sidebar').tooltip(
- selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
- placement: 'right'
- container: 'body'
- )
-
# Form submitter
$('.trigger-submit').on 'change', ->
$(@).parents('form').submit()
@@ -182,7 +176,7 @@ $ ->
flash.show()
# Disable form buttons while a form is submitting
- $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
+ $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
buttons = $('[type="submit"]', @)
switch e.type
@@ -191,50 +185,62 @@ $ ->
else
buttons.enable()
+ $(document).ajaxError (e, xhrObj, xhrSetting, xhrErrorText) ->
+
+ if xhrObj.status is 401
+ new Flash 'You need to be logged in.', 'alert'
+
+ else if xhrObj.status in [ 404, 500 ]
+ new Flash 'Something went wrong on our end.', 'alert'
+
+
# Show/Hide the profile menu when hovering the account box
$('.account-box').hover -> $(@).toggleClass('hover')
# Commit show suppressed diff
- $(document).on 'click', '.diff-content .js-show-suppressed-diff', ->
+ $document.on 'click', '.diff-content .js-show-suppressed-diff', ->
$container = $(@).parent()
$container.next('table').show()
$container.remove()
$('.navbar-toggle').on 'click', ->
$('.header-content .title').toggle()
+ $('.header-content .header-logo').toggle()
$('.header-content .navbar-collapse').toggle()
$('.navbar-toggle').toggleClass('active')
- $('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
# Show/hide comments on diff
- $("body").on "click", ".js-toggle-diff-comments", (e) ->
+ $body.on "click", ".js-toggle-diff-comments", (e) ->
$(@).toggleClass('active')
$(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault()
- $(document).off "click", '.js-confirm-danger'
- $(document).on "click", '.js-confirm-danger', (e) ->
+ $document.off "click", '.js-confirm-danger'
+ $document.on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
text = btn.data("confirm-danger-message")
form = btn.closest("form")
new ConfirmDangerModal(form, text)
+
+ $document.on 'click', 'button', ->
+ $(this).blur()
+
$('input[type="search"]').each ->
$this = $(this)
$this.attr 'value', $this.val()
return
- $(document)
+ $document
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
$this = $(this)
$this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
- $navIconToggle = $('.toggle-nav-collapse')
- $(document)
+ $document
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
@@ -242,57 +248,66 @@ $ ->
if $gutterIcon.hasClass('fa-angle-double-right')
$sidebarGutterToggle.trigger('click')
- $navIcon = $navIconToggle.find('.fa')
- if $navIcon.hasClass('fa-angle-left')
- $navIconToggle.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])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
- $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
- $(window)
- .off "resize"
- .on "resize", (e) ->
+ $window
+ .off "resize.app"
+ .on "resize.app", (e) ->
fitSidebarForSize()
+ gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize()
new Aside()
+
+ # Sidenav pinning
+ if $window.width() < 1024 and $.cookie('pin_nav') is 'true'
+ $.cookie('pin_nav', 'false', { path: '/', expires: 365 * 10 })
+ $('.page-with-sidebar')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ .removeClass('page-sidebar-pinned')
+ $('.navbar-fixed-top').removeClass('header-pinned-nav')
+
+ $document
+ .off 'click', '.js-nav-pin'
+ .on 'click', '.js-nav-pin', (e) ->
+ e.preventDefault()
+
+ $pinBtn = $(e.currentTarget)
+ $page = $ '.page-with-sidebar'
+ $topNav = $ '.navbar-fixed-top'
+ $tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
+ doPinNav = not $page.is('.page-sidebar-pinned')
+ tooltipText = 'Pin navigation'
+
+ $(this).toggleClass 'is-active'
+
+ if doPinNav
+ $page.addClass('page-sidebar-pinned')
+ $topNav.addClass('header-pinned-nav')
+ else
+ $tooltip.remove() # Remove it immediately when collapsing the sidebar
+ $page.removeClass('page-sidebar-pinned')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ $topNav.removeClass('header-pinned-nav')
+ .toggleClass('header-collapsed header-expanded')
+
+ # Save settings
+ $.cookie 'pin_nav', doPinNav, { path: '/', expires: 365 * 10 }
+
+ if $.cookie('pin_nav') is 'true' or doPinNav
+ tooltipText = 'Unpin navigation'
+
+ # Update tooltip text immediately
+ $tooltip.find('.tooltip-inner').text(tooltipText)
+
+ # Persist tooltip title
+ $pinBtn.attr('title', tooltipText).tooltip('fixTitle')
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index bf95e06b4e5..37d0adaa625 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,201 +1,356 @@
class @AwardsHandler
- constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
- $('.js-add-award').on 'click', (event) =>
- event.stopPropagation()
- event.preventDefault()
- @showEmojiMenu()
+ constructor: ->
- $('html').on 'click', (event) ->
- if !$(event.target).closest('.emoji-menu').length
+ @aliases = gl.emojiAliases()
+
+ $(document)
+ .off 'click', '.js-add-award'
+ .on 'click', '.js-add-award', (e) =>
+ e.stopPropagation()
+ e.preventDefault()
+
+ @showEmojiMenu $(e.currentTarget)
+
+ $('html').on 'click', (e) ->
+ $target = $ e.target
+
+ unless $target.closest('.emoji-menu-content').length
+ $('.js-awards-block.current').removeClass 'current'
+
+ unless $target.closest('.emoji-menu').length
if $('.emoji-menu').is(':visible')
+ $('.js-add-award.is-active').removeClass 'is-active'
$('.emoji-menu').removeClass 'is-visible'
- $('.awards')
- .off 'click'
- .on 'click', '.js-emoji-btn', @handleClick
+ $(document)
+ .off 'click', '.js-emoji-btn'
+ .on 'click', '.js-emoji-btn', (e) =>
+ e.preventDefault()
- @renderFrequentlyUsedBlock()
+ $target = $ e.currentTarget
+ emoji = $target.find('.icon').data 'emoji'
- handleClick: (e) ->
- e.preventDefault()
- emoji = $(this)
- .find('.icon')
- .data 'emoji'
+ $target.closest('.js-awards-block').addClass 'current'
+ @addAward @getVotesBlock(), @getAwardUrl(), emoji
- if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
- awardsHandler.addAward 'thumbsdown'
- else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
- awardsHandler.addAward 'thumbsup'
+ showEmojiMenu: ($addBtn) ->
- awardsHandler.addAward emoji
+ $menu = $ '.emoji-menu'
- $(this).trigger 'blur'
+ if $addBtn.hasClass 'js-note-emoji'
+ $addBtn.closest('.note').find('.js-awards-block').addClass 'current'
+ else
+ $addBtn.closest('.js-awards-block').addClass 'current'
- didUserClickEmoji: (that, emoji) ->
- if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
- $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
+ if $menu.length
+ $holder = $addBtn.closest('.js-award-holder')
- showEmojiMenu: ->
- if $('.emoji-menu').length
- if $('.emoji-menu').is '.is-visible'
- $('.emoji-menu').removeClass 'is-visible'
+ if $menu.is '.is-visible'
+ $addBtn.removeClass 'is-active'
+ $menu.removeClass 'is-visible'
$('#emoji_search').blur()
else
- $('.emoji-menu').addClass 'is-visible'
+ $addBtn.addClass 'is-active'
+ @positionMenu($menu, $addBtn)
+
+ $menu.addClass 'is-visible'
$('#emoji_search').focus()
else
- $('.js-add-award').addClass 'is-loading'
- $.get @getEmojisUrl, (response) =>
- $('.js-add-award').removeClass 'is-loading'
- $('.js-award-holder').append response
+ $addBtn.addClass 'is-loading is-active'
+ url = @getAwardMenuUrl()
+
+ @createEmojiMenu url, =>
+ $addBtn.removeClass 'is-loading'
+ $menu = $('.emoji-menu')
+ @positionMenu($menu, $addBtn)
+ @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered
+
setTimeout =>
- $('.emoji-menu').addClass 'is-visible'
+ $menu.addClass 'is-visible'
$('#emoji_search').focus()
@setupSearch()
, 200
- addAward: (emoji) ->
- @postEmoji emoji, =>
- @addAwardToEmojiBar(emoji)
+
+ createEmojiMenu: (awardMenuUrl, callback) ->
+
+ $.get awardMenuUrl, (response) ->
+ $('body').append response
+ callback()
+
+
+ positionMenu: ($menu, $addBtn) ->
+
+ position = $addBtn.data('position')
+
+ # The menu could potentially be off-screen or in a hidden overflow element
+ # So we position the element absolute in the body
+ css =
+ top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
+
+ if position? and position is 'right'
+ css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
+ $menu.addClass 'is-aligned-right'
+ else
+ css.left = "#{$addBtn.offset().left}px"
+ $menu.removeClass 'is-aligned-right'
+
+ $menu.css(css)
+
+
+ addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) ->
+
+ emoji = @normilizeEmojiName emoji
+
+ @postEmoji awardUrl, emoji, =>
+ @addAwardToEmojiBar votesBlock, emoji, checkMutuality
+ callback?()
$('.emoji-menu').removeClass 'is-visible'
- addAwardToEmojiBar: (emoji) ->
- @addEmojiToFrequentlyUsedList(emoji)
- if @exist(emoji)
- if @isActive(emoji)
- @decrementCounter(emoji)
+ addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) ->
+
+ @checkMutuality votesBlock, emoji if checkForMutuality
+ @addEmojiToFrequentlyUsedList emoji
+
+ emoji = @normilizeEmojiName emoji
+ $emojiButton = @findEmojiIcon(votesBlock, emoji).parent()
+
+ if $emojiButton.length > 0
+ if @isActive $emojiButton
+ @decrementCounter $emojiButton, emoji
else
- counter = @findEmojiIcon(emoji).siblings('.js-counter')
- counter.text(parseInt(counter.text()) + 1)
- counter.parent().addClass('active')
- @addMeToAuthorList(emoji)
+ counter = $emojiButton.find '.js-counter'
+ counter.text parseInt(counter.text()) + 1
+ $emojiButton.addClass 'active'
+ @addMeToUserList votesBlock, emoji
+ @animateEmoji $emojiButton
else
- @createEmoji(emoji)
-
- exist: (emoji) ->
- @findEmojiIcon(emoji).length > 0
-
- isActive: (emoji) ->
- @findEmojiIcon(emoji).parent().hasClass('active')
-
- decrementCounter: (emoji) ->
- 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'
- emojiIcon.tooltip('destroy')
- counter.text(0)
- emojiIcon.removeClass('active')
- @removeMeFromAuthorList(emoji)
+ votesBlock.removeClass 'hidden'
+ @createEmoji votesBlock, emoji
+
+
+ getVotesBlock: ->
+
+ currentBlock = $ '.js-awards-block.current'
+ return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0
+
+
+ getAwardUrl: -> return @getVotesBlock().data 'award-url'
+
+
+ checkMutuality: (votesBlock, emoji) ->
+
+ awardUrl = @getAwardUrl()
+
+ if emoji in [ 'thumbsup', 'thumbsdown' ]
+ mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
+ $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent()
+ isAlreadyVoted = $emojiButton.hasClass 'active'
+
+ if isAlreadyVoted
+ @showEmojiLoader $emojiButton
+ @addAward votesBlock, awardUrl, mutualVote, false, ->
+ $emojiButton.removeClass 'is-loading'
+
+
+ showEmojiLoader: ($emojiButton) ->
+
+ $loader = $emojiButton.find '.fa-spinner'
+
+ unless $loader.length
+ $emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'
+
+ $emojiButton.addClass 'is-loading'
+
+
+ isActive: ($emojiButton) -> $emojiButton.hasClass 'active'
+
+
+ decrementCounter: ($emojiButton, emoji) ->
+
+ counter = $ '.js-counter', $emojiButton
+ counterNumber = parseInt counter.text(), 10
+
+ if counterNumber > 1
+ counter.text counterNumber - 1
+ @removeMeFromUserList $emojiButton, emoji
+ else if emoji is 'thumbsup' or emoji is 'thumbsdown'
+ $emojiButton.tooltip 'destroy'
+ counter.text '0'
+ @removeMeFromUserList $emojiButton, emoji
+ @removeEmoji $emojiButton if $emojiButton.parents('.note').length
else
- emojiIcon.tooltip('destroy')
- emojiIcon.remove()
-
- removeMeFromAuthorList: (emoji) ->
- awardBlock = @findEmojiIcon(emoji).parent()
- authors = awardBlock
- .attr('data-original-title')
- .split(', ')
- authors.splice(authors.indexOf('me'),1)
+ @removeEmoji $emojiButton
+
+ $emojiButton.removeClass 'active'
+
+
+ removeEmoji: ($emojiButton) ->
+
+ $emojiButton.tooltip('destroy')
+ $emojiButton.remove()
+
+ $votesBlock = @getVotesBlock()
+
+ if $votesBlock.find('.js-emoji-btn').length is 0
+ $votesBlock.addClass 'hidden'
+
+
+ getAwardTooltip: ($awardBlock) ->
+
+ return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or ''
+
+
+ removeMeFromUserList: ($emojiButton, emoji) ->
+
+ awardBlock = $emojiButton
+ originalTitle = @getAwardTooltip awardBlock
+
+ authors = originalTitle.split ', '
+ authors.splice authors.indexOf('me'), 1
+
+ newAuthors = authors.join ', '
+
awardBlock
- .closest('.js-emoji-btn')
- .attr('data-original-title', authors.join(', '))
- @resetTooltip(awardBlock)
-
- addMeToAuthorList: (emoji) ->
- awardBlock = @findEmojiIcon(emoji).parent()
- origTitle = awardBlock.attr('data-original-title').trim()
- authors = []
+ .closest '.js-emoji-btn'
+ .removeData 'original-title'
+ .attr 'data-original-title', newAuthors
+
+ @resetTooltip awardBlock
+
+
+ addMeToUserList: (votesBlock, emoji) ->
+
+ awardBlock = @findEmojiIcon(votesBlock, emoji).parent()
+ origTitle = @getAwardTooltip awardBlock
+ users = []
+
if origTitle
- authors = origTitle.split(', ')
- authors.push('me')
- awardBlock.attr('data-original-title', authors.join(', '))
- @resetTooltip(awardBlock)
+ users = origTitle.trim().split ', '
+
+ users.push 'me'
+ awardBlock.attr 'title', users.join ', '
+
+ @resetTooltip awardBlock
+
resetTooltip: (award) ->
- award.tooltip('destroy')
- # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
- setTimeout (->
- award.tooltip()
- ), 200
+ award.tooltip 'destroy'
+
+ # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
+ cb = -> award.tooltip()
+ setTimeout cb, 200
+
+ createEmoji_: (votesBlock, emoji) ->
- createEmoji: (emoji) ->
- emojiCssClass = @resolveNameToCssClass(emoji)
+ emojiCssClass = @resolveNameToCssClass emoji
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
+ <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
+ <span class='award-control-text js-counter'>1</span>
+ </button>"
- nodes = []
- nodes.push(
- "<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
- "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
- "<span class='award-control-text js-counter'>1</span>",
- "</button>"
- )
+ $emojiButton = $ buttonHtml
+ $emojiButton
+ .insertBefore votesBlock.find '.js-award-holder'
+ .find '.emoji-icon'
+ .data 'emoji', emoji
- $(nodes.join("\n"))
- .insertBefore('.js-award-holder')
- .find('.emoji-icon')
- .data('emoji', emoji)
+ @animateEmoji $emojiButton
$('.award-control').tooltip()
+ votesBlock.removeClass 'current'
+
+
+ animateEmoji: ($emoji) ->
+
+ className = 'pulse animated'
+
+ $emoji.addClass className
+ setTimeout (-> $emoji.removeClass className), 321
+
+
+ createEmoji: (votesBlock, emoji) ->
+
+ if $('.emoji-menu').length
+ return @createEmoji_ votesBlock, emoji
+
+ @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji
+
+
+ getAwardMenuUrl: -> return gon.award_menu_url
+
resolveNameToCssClass: (emoji) ->
- emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
+
+ emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']"
if emojiIcon.length > 0
- unicodeName = emojiIcon.data('unicode-name')
+ unicodeName = emojiIcon.data 'unicode-name'
else
# Find by alias
- unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
+ unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name'
- "emoji-#{unicodeName}"
+ return "emoji-#{unicodeName}"
- postEmoji: (emoji, callback) ->
- $.post @postEmojiUrl, { note: {
- note: ":#{emoji}:"
- noteable_type: @noteableType
- noteable_id: @noteableId
- }},(data) ->
- if data.ok
- callback.call()
- findEmojiIcon: (emoji) ->
- $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
+ postEmoji: (awardUrl, emoji, callback) ->
+
+ $.post awardUrl, { name: emoji }, (data) ->
+ callback() if data.ok
+
+
+ findEmojiIcon: (votesBlock, emoji) ->
+
+ return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']"
+
scrollToAwards: ->
- $('body, html').animate({
- scrollTop: $('.awards').offset().top - 80
- }, 200)
+
+ options = scrollTop: $('.awards').offset().top - 110
+ $('body, html').animate options, 200
+
+
+ normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji
+
addEmojiToFrequentlyUsedList: (emoji) ->
+
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
- frequentlyUsedEmojis.push(emoji)
- $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 })
+ frequentlyUsedEmojis.push emoji
+ $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }
+
getFrequentlyUsedEmojis: ->
- frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',')
- _.compact(_.uniq(frequentlyUsedEmojis))
+
+ frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',')
+ return _.compact _.uniq frequentlyUsedEmojis
+
renderFrequentlyUsedBlock: ->
- if $.cookie('frequently_used_emojis')
+
+ if $.cookie 'frequently_used_emojis'
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
- ul = $('<ul>')
+ ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>")
for emoji in frequentlyUsedEmojis
- do (emoji) ->
- $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
+ $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
+
+ $('.emoji-menu-content')
+ .prepend(ul)
+ .prepend($('<h5>').text('Frequently used'))
+
+ @frequentEmojiBlockRendered = true
- $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
setupSearch: ->
- $('input.emoji-search').keyup (ev) =>
+
+ $('input.emoji-search').on 'keyup', (ev) =>
term = $(ev.target).val()
# Clean previous search results
@@ -203,13 +358,15 @@ class @AwardsHandler
if term
# Generate a search result block
- h5 = $('<h5>').text('Search results').addClass('emoji-search')
- foundEmojis = @searchEmojis(term).show()
- ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis)
+ h5 = $('<h5>').text('Search results')
+ found_emojis = @searchEmojis(term).show()
+ 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
$('.emoji-menu-content').children().show()
- searchEmojis: (term)->
- $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
+
+ searchEmojis: (term) ->
+
+ $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone()
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
new file mode 100644
index 00000000000..d9a03d05529
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
@@ -0,0 +1,23 @@
+#= require blob/template_selector
+
+class @BlobCiYamlSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
+
+class @BlobCiYamlSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitlab-ci-yml-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobCiYamlSelector(
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
new file mode 100644
index 00000000000..8d0e3f363d1
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
@@ -0,0 +1,5 @@
+#= require blob/template_selector
+
+class @BlobGitignoreSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitignoreText query.name, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
new file mode 100644
index 00000000000..a719ba25122
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobGitignoreSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitignore-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobGitignoreSelector(
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
index e17eaa75dc1..a3cc8dd844c 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee
@@ -1,30 +1,9 @@
-class @BlobLicenseSelector
- licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
+#= require blob/template_selector
- constructor: (editor) ->
- @$licenseSelector = $('.js-license-selector')
- $fileNameInput = $('#file_name')
+class @BlobLicenseSelector extends TemplateSelector
+ requestFile: (query) ->
+ data =
+ project: @dropdown.data('project')
+ fullname: @dropdown.data('fullname')
- initialFileNameValue = if $fileNameInput.length
- $fileNameInput.val()
- else if $('.editor-file-name').length
- $('.editor-file-name').text().trim()
-
- @toggleLicenseSelector(initialFileNameValue)
-
- if $fileNameInput
- $fileNameInput.on 'keyup blur', (e) =>
- @toggleLicenseSelector($(e.target).val())
-
- $('select.license-select').on 'change', (e) ->
- data =
- project: $(this).data('project')
- fullname: $(this).data('fullname')
- Api.licenseText $(this).val(), data, (license) ->
- editor.setValue(license.content, -1)
-
- toggleLicenseSelector: (fileName) =>
- if @licenseRegex.test(fileName)
- @$licenseSelector.show()
- else
- @$licenseSelector.hide()
+ Api.licenseText query.id, data, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
new file mode 100644
index 00000000000..68438733108
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobLicenseSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-license-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobLicenseSelector(
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index eea9aa972ee..19e584519d7 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -12,7 +12,10 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
- new BlobLicenseSelector(@editor)
+
+ new BlobLicenseSelectors { @editor }
+ new BlobGitignoreSelectors { @editor }
+ new BlobCiYamlSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee
new file mode 100644
index 00000000000..40c9169beac
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.coffee
@@ -0,0 +1,60 @@
+class @TemplateSelector
+ constructor: (opts = {}) ->
+ {
+ @dropdown,
+ @data,
+ @pattern,
+ @wrapper,
+ @editor,
+ @fileEndpoint,
+ @$input = $('#file_name')
+ } = opts
+
+ @buildDropdown()
+ @bindEvents()
+ @onFilenameUpdate()
+
+ buildDropdown: ->
+ @dropdown.glDropdown(
+ data: @data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: @toggleLabel,
+ search:
+ fields: ['name']
+ clicked: @onClick
+ text: (item) ->
+ item.name
+ )
+
+ bindEvents: ->
+ @$input.on('keyup blur', (e) =>
+ @onFilenameUpdate()
+ )
+
+ toggleLabel: (item) ->
+ item.name
+
+ onFilenameUpdate: ->
+ return unless @$input.length
+
+ filenameMatches = @pattern.test(@$input.val().trim())
+
+ if not filenameMatches
+ @wrapper.addClass('hidden')
+ return
+
+ @wrapper.removeClass('hidden')
+
+ onClick: (item, el, e) =>
+ e.preventDefault()
+ @requestFile(item)
+
+ requestFile: (item) ->
+ # To be implemented on the extending class
+ # e.g.
+ # Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+
+ requestFileSuccess: (file) ->
+ @editor.setValue(file.content, 1)
+ @editor.focus()
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
deleted file mode 100644
index d80e0e716ce..00000000000
--- a/app/assets/javascripts/calendar.js.coffee
+++ /dev/null
@@ -1,34 +0,0 @@
-class @Calendar
- constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
- cal = new CalHeatMap()
- cal.init
- itemName: ["contribution"]
- data: timestamps
- start: new Date(starting_year, starting_month)
- domainLabelFormat: "%b"
- id: "cal-heatmap"
- domain: "month"
- subDomain: "day"
- range: 12
- tooltip: true
- label:
- position: "top"
- legend: [
- 0
- 10
- 20
- 30
- ]
- legendCellPadding: 3
- cellSize: $('.user-calendar').width() / 73
- onClick: (date, count) ->
- formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
- $.ajax
- url: calendar_activities_path
- data:
- date: formated_date
- cache: false
- dataType: "html"
- success: (data) ->
- $(".user-calendar-activities").html data
-
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 7afe8bf79e2..74691b2c1b5 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -1,16 +1,33 @@
-class CiBuild
+class @CiBuild
@interval: null
+ @state: null
- constructor: (build_url, build_status) ->
+ constructor: (@page_url, @build_url, @build_status, @state) ->
clearInterval(CiBuild.interval)
- @initScrollButtonAffix()
+ # Init breakpoint checker
+ @bp = Breakpoints.get()
+ @hideSidebar()
+ $('.js-build-sidebar').niceScroll()
+ $(document)
+ .off 'click', '.js-sidebar-build-toggle'
+ .on 'click', '.js-sidebar-build-toggle', @toggleSidebar
- if build_status == "running" || build_status == "pending"
+ $(window)
+ .off 'resize.build'
+ .on 'resize.build', @hideSidebar
+
+ @updateArtifactRemoveDate()
+
+ if $('#build-trace').length
+ @getInitialBuildTrace()
+ @initScrollButtonAffix()
+
+ if @build_status is "running" or @build_status is "pending"
#
# Bind autoscroll button to follow build output
#
- $("#autoscroll-button").bind "click", ->
+ $('#autoscroll-button').on 'click', ->
state = $(this).data("state")
if "enabled" is state
$(this).data "state", "disabled"
@@ -24,19 +41,37 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
- if window.location.href.split("#").first() is build_url
- $.ajax
- url: build_url
- dataType: "json"
- success: (build) =>
- if build.status == "running"
- $('#build-trace code').html build.trace_html
- $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
- @checkAutoscroll()
- else if build.status != build_status
- Turbolinks.visit build_url
+ if window.location.href.split("#").first() is @page_url
+ @getBuildTrace()
, 4000
+ getInitialBuildTrace: ->
+ $.ajax
+ url: @build_url
+ dataType: 'json'
+ success: (build_data) ->
+ $('.js-build-output').html build_data.trace_html
+
+ if build_data.status is 'success' or build_data.status is 'failed'
+ $('.js-build-refresh').remove()
+
+ getBuildTrace: ->
+ $.ajax
+ url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}"
+ dataType: "json"
+ success: (log) =>
+ if log.state
+ @state = log.state
+
+ if log.status is "running"
+ if log.append
+ $('.js-build-output').append log.html
+ else
+ $('.js-build-output').html log.html
+ @checkAutoscroll()
+ else if log.status isnt @build_status
+ Turbolinks.visit @page_url
+
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
@@ -51,4 +86,29 @@ class CiBuild
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
-@CiBuild = CiBuild
+ shouldHideSidebar: ->
+ bootstrapBreakpoint = @bp.getBreakpointSize()
+
+ bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm'
+
+ toggleSidebar: =>
+ if @shouldHideSidebar()
+ $('.js-build-sidebar')
+ .toggleClass 'right-sidebar-expanded right-sidebar-collapsed'
+
+ hideSidebar: =>
+ if @shouldHideSidebar()
+ $('.js-build-sidebar')
+ .removeClass 'right-sidebar-expanded'
+ .addClass 'right-sidebar-collapsed'
+ else
+ $('.js-build-sidebar')
+ .removeClass 'right-sidebar-collapsed'
+ .addClass 'right-sidebar-expanded'
+
+ updateArtifactRemoveDate: ->
+ $date = $('.js-artifacts-remove')
+
+ if $date.length
+ date = $date.text()
+ $date.text $.timefor(new Date(date), ' ')
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index f91aa3c5ad7..a39df421832 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -16,8 +16,8 @@ class Dispatcher
shortcut_handler = null
switch page
when 'projects:issues:index'
- Issues.init()
Issuable.init()
+ new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
@@ -29,6 +29,7 @@ class Dispatcher
new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
+ new DueDateSelect()
new GLForm($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
@@ -53,9 +54,13 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
+ new MergedButtons()
+ when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
+ new MergedButtons()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
+ new MergedButtons()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
Issuable.init()
@@ -68,18 +73,19 @@ class Dispatcher
new Diff()
new ZenMode()
shortcut_handler = new ShortcutsNavigation()
- when 'projects:commits:show'
- shortcut_handler = new ShortcutsNavigation()
- when 'projects:activity'
+ when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
+ new NotificationsForm()
new TreeView() if $('#tree-slider').length
when 'groups:activity'
new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
+ new NotificationsForm()
+ new NotificationsDropdown()
when 'groups:group_members:index'
new GroupMembers()
new UsersSelect()
@@ -96,8 +102,11 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
+ new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
+ when 'projects:labels:index'
+ new LabelManager() if $('.prioritized-labels').length
when 'projects:network:show'
# Ensure we don't create a particular shortcut handler here. This is
# already created, where the network graph is created.
@@ -118,24 +127,26 @@ class Dispatcher
when 'groups'
new UsersSelect()
when 'projects'
- new NamespaceSelect()
- when 'dashboard'
+ new NamespaceSelects()
+ when 'dashboard', 'root'
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
new Profile()
+ new NotificationsForm()
+ new NotificationsDropdown()
when 'projects'
new Project()
new ProjectAvatar()
switch path[1]
- when 'compare'
- shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
when 'new'
new ProjectNew()
when 'show'
+ new ProjectNew()
new ProjectShow()
+ new NotificationsDropdown()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
@@ -144,9 +155,9 @@ class Dispatcher
when 'snippets'
shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show'
- when 'labels', 'graphs'
- shortcut_handler = new ShortcutsNavigation()
- when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
+ when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
+ 'milestones', 'project_members', 'deploy_keys', 'builds', \
+ 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index e2194589b38..665246e2a7d 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -70,12 +70,12 @@ class @DropzoneInput
pasteText response.link.markdown
return
- error: (temp, errorMessage) ->
+ error: (temp) ->
errorAlert = $(form).find('.error-alert')
checkIfMsgExists = errorAlert.children().length
if checkIfMsgExists is 0
errorAlert.append divAlert
- $(".div-dropzone-alert").append btnAlert + errorMessage
+ $(".div-dropzone-alert").append "#{btnAlert}Attaching the file failed."
return
totaluploadprogress: (totalUploadProgress) ->
diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
index a4304786cbb..d65c018dad5 100644
--- a/app/assets/javascripts/due_date_select.js.coffee
+++ b/app/assets/javascripts/due_date_select.js.coffee
@@ -1,5 +1,21 @@
class @DueDateSelect
constructor: ->
+ # Milestone edit/new form
+ $datePicker = $('.datepicker')
+
+ if $datePicker.length
+ $dueDate = $('#milestone_due_date')
+ $datePicker.datepicker
+ dateFormat: 'yy-mm-dd'
+ onSelect: (dateText, inst) ->
+ $dueDate.val(dateText)
+ .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
+
+ $('.js-clear-due-date').on 'click', (e) ->
+ e.preventDefault()
+ $.datepicker._clearDate($datePicker)
+
+ # Issuable sidebar
$loading = $('.js-issuable-update .due_date')
.find('.block-loading')
.hide()
@@ -11,6 +27,7 @@ class @DueDateSelect
$block = $dropdown.closest('.block')
$selectbox = $dropdown.closest('.selectbox')
$value = $block.find('.value')
+ $valueContent = $block.find('.value-content')
$sidebarValue = $('.js-due-date-sidebar-value', $block)
fieldName = $dropdown.data('field-name')
@@ -20,14 +37,18 @@ class @DueDateSelect
$dropdown.glDropdown(
hidden: ->
$selectbox.hide()
- $value.removeAttr('style')
+ $value.css('display', '')
)
- addDueDate = ->
+ addDueDate = (isDropdown) ->
# Create the post date
value = $("input[name='#{fieldName}']").val()
- date = new Date value.replace(new RegExp('-', 'g'), ',')
- mediumDate = $.datepicker.formatDate 'M d, yy', date
+
+ if value isnt ''
+ date = new Date value.replace(new RegExp('-', 'g'), ',')
+ mediumDate = $.datepicker.formatDate 'M d, yy', date
+ else
+ mediumDate = 'No due date'
data = {}
data[abilityName] = {}
@@ -37,25 +58,39 @@ class @DueDateSelect
type: 'PUT'
url: issueUpdateURL
data: data
+ dataType: 'json'
beforeSend: ->
$loading.fadeIn()
- $dropdown.trigger('loading.gl.dropdown')
- $selectbox.hide()
- $value.removeAttr('style')
+ if isDropdown
+ $dropdown.trigger('loading.gl.dropdown')
+ $selectbox.hide()
+ $value.css('display', '')
- $value.html(mediumDate)
+ cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
+ $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
$sidebarValue.html(mediumDate)
+
+ if value isnt ''
+ $('.js-remove-due-date-holder').removeClass 'hidden'
+ else
+ $('.js-remove-due-date-holder').addClass 'hidden'
).done (data) ->
- $dropdown.trigger('loaded.gl.dropdown')
- $dropdown.dropdown('toggle')
+ if isDropdown
+ $dropdown.trigger('loaded.gl.dropdown')
+ $dropdown.dropdown('toggle')
$loading.fadeOut()
+ $block.on 'click', '.js-remove-due-date', (e) ->
+ e.preventDefault()
+ $("input[name='#{fieldName}']").val ''
+ addDueDate(false)
+
$datePicker.datepicker(
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='#{fieldName}']").val()
altField: "input[name='#{fieldName}']"
onSelect: ->
- addDueDate()
+ addDueDate(true)
)
$(document)
diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee
index 5de012e409f..b76d214790a 100644
--- a/app/assets/javascripts/flash.js.coffee
+++ b/app/assets/javascripts/flash.js.coffee
@@ -1,14 +1,22 @@
class @Flash
- constructor: (message, type)->
+ constructor: (message, type = 'alert')->
@flash = $(".flash-container")
@flash.html("")
innerDiv = $('<div/>',
- class: "flash-#{type}",
- text: message
+ class: "flash-#{type}"
)
innerDiv.appendTo(".flash-container")
+ textDiv = $("<div/>",
+ class: "flash-text",
+ text: message
+ )
+ textDiv.appendTo(innerDiv)
+
+ if @flash.parent().hasClass('content-wrapper')
+ textDiv.addClass('container-fluid container-limited')
+
@flash.click -> $(@).fadeOut()
@flash.show()
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 61e3f811e73..4a851d9c9fb 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -3,7 +3,8 @@
window.GitLab ?= {}
GitLab.GfmAutoComplete =
dataLoading: false
-
+ dataLoaded: false
+ cachedData: {}
dataSource: ''
# Emoji
@@ -14,10 +15,35 @@ GitLab.GfmAutoComplete =
Members:
template: '<li>${username} <small>${title}</small></li>'
+ Labels:
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+
# Issues and MergeRequests
Issues:
template: '<li><small>${id}</small> ${title}</li>'
+ # Milestones
+ Milestones:
+ template: '<li>${title}</li>'
+
+ Loading:
+ template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+
+ DefaultOptions:
+ sorter: (query, items, searchKey) ->
+ return items if items[0].name? and items[0].name is 'loading'
+
+ $.fn.atwho.default.callbacks.sorter(query, items, searchKey)
+ filter: (query, data, searchKey) ->
+ return data if data[0] is 'loading'
+
+ $.fn.atwho.default.callbacks.filter(query, data, searchKey)
+ beforeInsert: (value) ->
+ if not GitLab.GfmAutoComplete.dataLoaded
+ @at
+ else
+ value
+
# Add GFM auto-completion to all input fields, that accept GFM input.
setup: (wrap) ->
@input = $('.js-gfm-input')
@@ -29,7 +55,7 @@ GitLab.GfmAutoComplete =
@setupAtWho()
if @dataSource
- if !@dataLoading
+ if not @dataLoading and not @cachedData
@dataLoading = true
# We should wait until initializations are done
@@ -44,23 +70,44 @@ GitLab.GfmAutoComplete =
@loadData(data)
, 1000)
+ if @cachedData?
+ @loadData(@cachedData)
setupAtWho: ->
# Emoji
@input.atwho
at: ':'
- displayTpl: @Emoji.template
+ displayTpl: (value) =>
+ if value.path?
+ @Emoji.template
+ else
+ @Loading.template
insertTpl: ':${name}:'
+ data: ['loading']
+ callbacks:
+ sorter: @DefaultOptions.sorter
+ filter: @DefaultOptions.filter
+ beforeInsert: @DefaultOptions.beforeInsert
# Team Members
@input.atwho
at: '@'
- displayTpl: @Members.template
+ displayTpl: (value) =>
+ if value.username?
+ @Members.template
+ else
+ @Loading.template
insertTpl: '${atwho-at}${username}'
searchKey: 'search'
+ data: ['loading']
callbacks:
+ sorter: @DefaultOptions.sorter
+ filter: @DefaultOptions.filter
+ beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (members) ->
$.map members, (m) ->
+ return m if not m.username?
+
title = m.name
title += " (#{m.count})" if m.count
@@ -72,28 +119,87 @@ GitLab.GfmAutoComplete =
at: '#'
alias: 'issues'
searchKey: 'search'
- displayTpl: @Issues.template
+ displayTpl: (value) =>
+ if value.title?
+ @Issues.template
+ else
+ @Loading.template
+ data: ['loading']
insertTpl: '${atwho-at}${id}'
callbacks:
+ sorter: @DefaultOptions.sorter
+ filter: @DefaultOptions.filter
+ beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (issues) ->
$.map issues, (i) ->
+ return i if not i.title?
+
id: i.iid
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
@input.atwho
+ at: '%'
+ alias: 'milestones'
+ searchKey: 'search'
+ displayTpl: (value) =>
+ if value.title?
+ @Milestones.template
+ else
+ @Loading.template
+ insertTpl: '${atwho-at}"${title}"'
+ data: ['loading']
+ callbacks:
+ beforeSave: (milestones) ->
+ $.map milestones, (m) ->
+ return m if not m.title?
+
+ id: m.iid
+ title: sanitize(m.title)
+ search: "#{m.title}"
+
+ @input.atwho
at: '!'
alias: 'mergerequests'
searchKey: 'search'
- displayTpl: @Issues.template
+ displayTpl: (value) =>
+ if value.title?
+ @Issues.template
+ else
+ @Loading.template
+ data: ['loading']
insertTpl: '${atwho-at}${id}'
callbacks:
+ sorter: @DefaultOptions.sorter
+ filter: @DefaultOptions.filter
+ beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (merges) ->
$.map merges, (m) ->
+ return m if not m.title?
+
id: m.iid
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
+ @input.atwho
+ at: '~'
+ alias: 'labels'
+ searchKey: 'search'
+ displayTpl: @Labels.template
+ insertTpl: '${atwho-at}${title}'
+ callbacks:
+ beforeSave: (merges) ->
+ sanitizeLabelTitle = (title)->
+ if /[\w\?&]+\s+[\w\?&]+/g.test(title)
+ "\"#{sanitize(title)}\""
+ else
+ sanitize(title)
+
+ $.map merges, (m) ->
+ title: sanitizeLabelTitle(m.title)
+ color: m.color
+ search: "#{m.title}"
+
destroyAtWho: ->
@input.atwho('destroy')
@@ -101,11 +207,22 @@ GitLab.GfmAutoComplete =
$.getJSON(dataSource)
loadData: (data) ->
+ @cachedData = data
+ @dataLoaded = true
+
# load members
@input.atwho 'load', '@', data.members
# load issues
@input.atwho 'load', 'issues', data.issues
+ # load milestones
+ @input.atwho 'load', 'milestones', data.milestones
# load merge requests
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
+ # load labels
+ @input.atwho 'load', '~', data.labels
+
+ # This trigger at.js again
+ # otherwise we would be stuck with loading until the user types
+ $(':focus').trigger('keyup')
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 1d1bfeb2e77..1c65e833d47 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
+ @indeterminateIds = []
+
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
@@ -35,34 +37,62 @@ class GitLabDropdownFilter
if keyCode is 13
return false
- clearTimeout timeout
- timeout = setTimeout =>
- blur_field = @shouldBlur keyCode
- search_text = @input.val()
+ # Only filter asynchronously only if option remote is set
+ if @options.remote
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur keyCode
- if blur_field and @filterInputBlur
- @input.blur()
+ if blur_field and @filterInputBlur
+ @input.blur()
- if @options.remote
- @options.query search_text, (data) =>
+ @options.query @input.val(), (data) =>
@options.callback(data)
- else
- @filter search_text
- , 250
+ , 250
+ else
+ @filter @input.val()
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
filter: (search_text) ->
+ @options.onFilter(search_text) if @options.onFilter
data = @options.data()
- if data?
+ if data? and not @options.filterByText
results = data
if search_text isnt ''
- results = fuzzaldrinPlus.filter(data, search_text,
- key: @options.keys
- )
+ # When data is an array of objects therefore [object Array] e.g.
+ # [
+ # { prop: 'foo' },
+ # { prop: 'baz' }
+ # ]
+ if _.isArray(data)
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+ else
+ # If data is grouped therefore an [object Object]. e.g.
+ # {
+ # groupName1: [
+ # { prop: 'foo' },
+ # { prop: 'baz' }
+ # ],
+ # groupName2: [
+ # { prop: 'abc' },
+ # { prop: 'def' }
+ # ]
+ # }
+ if gl.utils.isObject data
+ results = {}
+ for key, group of data
+ tmp = fuzzaldrinPlus.filter(group, search_text,
+ key: @options.keys
+ )
+
+ if tmp.length
+ results[key] = tmp.map (item) -> item
@options.callback results
else
@@ -73,10 +103,11 @@ class GitLabDropdownFilter
$el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
- if matches.length
- $el.show()
- else
- $el.hide()
+ unless $el.is('.dropdown-header')
+ if matches.length
+ $el.show()
+ else
+ $el.hide()
else
elements.show()
@@ -115,6 +146,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
+ INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
@@ -141,8 +173,9 @@ class GitLabDropdown
searchFields = if @options.search then @options.search.fields else [];
if @options.data
- # If data is an array
- if _.isArray @options.data
+ # If we provided data
+ # data could be an array of objects or a group of arrays
+ if _.isObject(@options.data) and not _.isFunction(@options.data)
@fullData = @options.data
@parseData @options.data
else
@@ -155,14 +188,15 @@ class GitLabDropdown
@parseData @fullData
- if @options.filterable
- @filterInput.trigger 'keyup'
+ @filter.input.trigger('keyup') if @options.filterable and @filter and @filter.input
}
# Init filterable
if @options.filterable
@filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
+ filterByText: @options.filterByText
+ onFilter: @options.onFilter
remote: @options.filterRemote
query: @options.data
keys: searchFields
@@ -183,10 +217,18 @@ class GitLabDropdown
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
+ $(@el).on "update.label", @updateLabel
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
@dropdown.on 'keyup', (e) =>
if e.which is 27 # Escape key
$('.dropdown-menu-close', @dropdown).trigger 'click'
+ @dropdown.on 'blur', 'a', (e) =>
+ if e.relatedTarget?
+ $relatedTarget = $(e.relatedTarget)
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu')
+
+ if $dropdownMenu.length is 0
+ @dropdown.removeClass('open')
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
@@ -230,19 +272,33 @@ class GitLabDropdown
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()]
+ else
+ # Handle array groups
+ if gl.utils.isObject data
+ html = []
+ for name, groupData of data
+ # Add header for each group
+ html.push(@renderItem(header: name, name))
+
+ @renderData(groupData, name)
+ .map (item) ->
+ html.push item
+ else
+ # Render each row
+ html = @renderData(data)
# Render the full menu
- full_html = @renderMenu(html.join(""))
+ full_html = @renderMenu(html)
@appendMenu(full_html)
+ renderData: (data, group = false) ->
+ data.map (obj, index) =>
+ return @renderItem(obj, group, index)
+
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
@@ -256,6 +312,16 @@ class GitLabDropdown
opened: =>
@addArrowKeyEvent()
+ if @options.setIndeterminateIds
+ @options.setIndeterminateIds.call(@)
+
+ if @options.setActiveIds
+ @options.setActiveIds.call(@)
+
+ # Makes indeterminate items effective
+ if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @parseData @fullData
+
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
@@ -267,12 +333,18 @@ class GitLabDropdown
hidden: (e) =>
@removeArrayKeyEvent()
+
+ $input = @dropdown.find(".dropdown-input-field")
+
if @options.filterable
- @dropdown
- .find(".dropdown-input-field")
+ $input
.blur()
.val("")
- .trigger("keyup")
+
+ # Triggering 'keyup' will re-render the dropdown which is not always required
+ # specially if we want to keep the state of the dropdown needed for bulk-assignment
+ if not @options.persistWhenHide
+ $input.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
@@ -290,7 +362,8 @@ class GitLabDropdown
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
- menu_html = "<ul>#{html}</ul>"
+ menu_html = $('<ul />')
+ .append(html)
return menu_html
@@ -299,11 +372,12 @@ class GitLabDropdown
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
-
- $(selector, @dropdown).html html
+ $(selector, @dropdown)
+ .empty()
+ .append(html)
# Render the row
- renderItem: (data) ->
+ renderItem: (data, group = false, index = false) ->
html = ""
# Divider
@@ -317,7 +391,7 @@ class GitLabDropdown
if @options.renderRow
# Call the render function
- html = @options.renderRow(data)
+ html = @options.renderRow.call(@options, data, @)
else
if not selected
value = if @options.id then @options.id(data) else data.id
@@ -346,8 +420,13 @@ class GitLabDropdown
if @highlight
text = @highlightTextMatches(text, @filterInput.val())
+ if group
+ groupAttrs = "data-group='#{group}' data-index='#{index}'"
+ else
+ groupAttrs = ''
+
html = "<li>
- <a href='#{url}' class='#{cssClass}'>
+ <a href='#{url}' #{groupAttrs} class='#{cssClass}'>
#{text}
</a>
</li>"
@@ -377,9 +456,15 @@ class GitLabDropdown
rowClicked: (el) ->
fieldName = @options.fieldName
- selectedIndex = el.parent().index()
if @renderedData
- selectedObject = @renderedData[selectedIndex]
+ groupName = el.data('group')
+ if groupName
+ selectedIndex = el.data('index')
+ selectedObject = @renderedData[groupName][selectedIndex]
+ else
+ selectedIndex = el.closest('li').index()
+ selectedObject = @renderedData[selectedIndex]
+
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS)
@@ -388,9 +473,20 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
- $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
+ @updateLabel(selectedObject, el, @)
else
selectedObject
+ else if el.hasClass(INDETERMINATE_CLASS)
+ el.addClass ACTIVE_CLASS
+ el.removeClass INDETERMINATE_CLASS
+
+ if not value?
+ field.remove()
+
+ if not field.length and fieldName
+ @addInput(fieldName, value)
+
+ return selectedObject
else
if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@@ -404,34 +500,45 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
- $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
+ @updateLabel(selectedObject, el, @)
if value?
if !field.length and fieldName
- # Create hidden input for form
- input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
- if @options.inputId?
- input = $(input)
- .attr('id', @options.inputId)
- @dropdown.before input
+ @addInput(fieldName, value)
else
field.val value
return selectedObject
- selectRowAtIndex: (index) ->
- selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
+ addInput: (fieldName, value)->
+ # Create hidden input for form
+ $input = $('<input>').attr('type', 'hidden')
+ .attr('name', fieldName)
+ .val(value)
+
+ if @options.inputId?
+ $input.attr('id', @options.inputId)
+
+ @dropdown.before $input
+
+ selectRowAtIndex: (e, index) ->
+ selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
- $(selector, @dropdown).trigger "click"
+ $el = $(selector, @dropdown)
+
+ if $el.length
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ $el.first().trigger('click')
addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field")
- selector = '.dropdown-content li:not(.divider)'
+ selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
@@ -459,8 +566,8 @@ class GitLabDropdown
return false
- if currentKeyCode is 13
- @selectRowAtIndex currentIndex
+ if currentKeyCode is 13 and currentIndex isnt -1
+ @selectRowAtIndex e, currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
@@ -492,6 +599,9 @@ class GitLabDropdown
# Scroll the dropdown content up
$dropdownContent.scrollTop(listItemTop - dropdownContentTop)
+ updateLabel: (selected = null, el = null, instance = null) =>
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el, instance)
+
$.fn.glDropdown = (opts) ->
return @.each ->
if (!$.data @, 'glDropdown')
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee
index d540cc4dc46..77512d187c9 100644
--- a/app/assets/javascripts/gl_form.js.coffee
+++ b/app/assets/javascripts/gl_form.js.coffee
@@ -34,6 +34,8 @@ class @GLForm
# form and textarea event listeners
@addEventListeners()
+ gl.text.init(@form)
+
# hide discard button
@form.find('.js-note-discard').hide()
@@ -42,6 +44,7 @@ class @GLForm
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
+ gl.text.removeListeners(@form)
addEventListeners: ->
@textarea.on 'focus', ->
diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee
new file mode 100644
index 00000000000..e0f681acf0b
--- /dev/null
+++ b/app/assets/javascripts/graphs/application.js.coffee
@@ -0,0 +1,7 @@
+# This is a manifest file that'll be compiled into including all the files listed below.
+# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+# be included in the compiled file accessible from http://example.com/assets/application.js
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+#= require_tree .
diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee
index f36c71fd25e..f36c71fd25e 100644
--- a/app/assets/javascripts/stat_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph.js.coffee
diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
index 3be14cb43dd..1d9fae7cf79 100644
--- a/app/assets/javascripts/stat_graph_contributors.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
@@ -1,5 +1,4 @@
#= require d3
-#= require stat_graph_contributors_util
class @ContributorsStatGraph
init: (log) ->
diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
index b7a0e073766..834a81af459 100644
--- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
@@ -1,6 +1,4 @@
#= require d3
-#= require jquery
-#= require underscore
class @ContributorsGraph
MARGIN:
@@ -123,7 +121,11 @@ class @ContributorsMasterGraph extends ContributorsGraph
class @ContributorsAuthorGraph extends ContributorsGraph
constructor: (@data) ->
- @width = $('.content').width()/2 - 100
+ # Don't split graph size in half for mobile devices.
+ if $(window).width() < 768
+ @width = $('.content').width() - 80
+ else
+ @width = ($('.content').width() / 2) - 100
@height = 200
@x = null
@y = null
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
index 31617c88b4a..31617c88b4a 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee
index b0edc895649..eb046eb2eff 100644
--- a/app/assets/javascripts/importer_status.js.coffee
+++ b/app/assets/javascripts/importer_status.js.coffee
@@ -7,13 +7,16 @@ class @ImporterStatus
$('.js-add-to-import')
.off 'click'
.on 'click', (e) =>
- new_namespace = null
$btn = $(e.currentTarget)
$tr = $btn.closest('tr')
+ $target_field = $tr.find('.import-target')
+ $namespace_input = $target_field.find('input')
id = $tr.attr('id').replace('repo_', '')
- if $tr.find('.import-target input').length > 0
- new_namespace = $tr.find('.import-target input').prop('value')
- $tr.find('.import-target').empty().append("#{new_namespace} / #{$tr.find('.import-target').data('project_name')}")
+ new_namespace = null
+
+ if $namespace_input.length > 0
+ new_namespace = $namespace_input.prop('value')
+ $target_field.empty().append("#{new_namespace}/#{$target_field.data('project_name')}")
$btn
.disable()
diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee
index afffed63ac5..c71d4ecf505 100644
--- a/app/assets/javascripts/issuable.js.coffee
+++ b/app/assets/javascripts/issuable.js.coffee
@@ -1,13 +1,23 @@
+issuable_created = false
@Issuable =
init: ->
- Issuable.initTemplates()
- Issuable.initSearch()
+ unless issuable_created
+ issuable_created = true
+ Issuable.initTemplates()
+ Issuable.initSearch()
+ Issuable.initChecks()
+ Issuable.initLabelFilterRemove()
initTemplates: ->
Issuable.labelRow = _.template(
'<% _.each(labels, function(label){ %>
- <span class="label-row">
- <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
+ <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;">
+ <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body">
+ <%- label.title %>
+ </a>
+ <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>">
+ <i class="fa fa-times"></i>
+ </button>
</span>
<% }); %>'
)
@@ -19,66 +29,67 @@
.on 'keyup', ->
clearTimeout(@timer)
@timer = setTimeout( ->
- Issuable.filterResults $('#issue_search_form')
+ $search = $('#issue_search')
+ $form = $('.js-filter-form')
+ $input = $("input[name='#{$search.attr('name')}']", $form)
+
+ if $input.length is 0
+ $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>"
+ else
+ $input.val $search.val()
+
+ Issuable.filterResults $form
, 500)
- toggleLabelFilters: ->
- $filteredLabels = $('.filtered-labels')
- if $filteredLabels.find('.label-row').length > 0
- $filteredLabels.removeClass('hidden')
- else
- $filteredLabels.addClass('hidden')
+ initLabelFilterRemove: ->
+ $(document)
+ .off 'click', '.js-label-filter-remove'
+ .on 'click', '.js-label-filter-remove', (e) ->
+ $button = $(@)
+
+ # Remove the label input box
+ $('input[name="label_name[]"]')
+ .filter -> @value is $button.data('label')
+ .remove()
+
+ # Submit the form to get new data
+ Issuable.filterResults $('.filter-form')
+ $('.js-label-select').trigger('update.label')
filterResults: (form) =>
formData = form.serialize()
- $('.issues-holder, .merge-requests-holder').css('opacity', '0.5')
formAction = form.attr('action')
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
- $.ajax
- type: 'GET'
- url: formAction
- data: formData
- complete: ->
- $('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
- success: (data) ->
- $('.issues-holder, .merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issuesUrl}, document.title, issuesUrl
- Issuable.reload()
- Issuable.updateStateFilters()
- $filteredLabels = $('.filtered-labels')
-
- if typeof Issuable.labelRow is 'function'
- $filteredLabels.html(Issuable.labelRow(data))
-
- Issuable.toggleLabelFilters()
-
- dataType: "json"
-
- reload: ->
- if Issues.created
- Issues.initChecks()
-
- $('#filter_issue_search').val($('#issue_search').val())
-
- updateStateFilters: ->
- stateFilters = $('.issues-state-filters')
- newParams = {}
- paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search']
-
- for paramKey in paramKeys
- newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
-
- if stateFilters.length
- stateFilters.find('a').each ->
- initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
- labelNameValues = gl.utils.getParameterValues('label_name[]')
- if labelNameValues
- labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
- newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
- else
- newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
- $(this).attr 'href', newUrl
+
+ Turbolinks.visit(issuesUrl)
+
+ initChecks: ->
+ @issuableBulkActions = $('.bulk-update').data('bulkActions')
+
+ $('.check_all_issues').off('click').on('click', ->
+ $('.selected_issue').prop('checked', @checked)
+ Issuable.checkChanged()
+ )
+
+ $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(@))
+
+
+ checkChanged: ->
+ checked_issues = $('.selected_issue:checked')
+ if checked_issues.length > 0
+ ids = $.map checked_issues, (value) ->
+ $(value).data('id')
+
+ $('#update_issues_ids').val ids
+ $('.issues-other-filters').hide()
+ $('.issues_bulk_update').show()
+ else
+ $('#update_issues_ids').val []
+ $('.issues_bulk_update').hide()
+ $('.issues-other-filters').show()
+ @issuableBulkActions.willUpdateLabels = false
+
+ return true
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 7a788f761b7..5b7a4831dfc 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -19,6 +19,16 @@ class @IssuableForm
@form.on "click", ".btn-cancel", @resetAutosave
@initWip()
+ @initMoveDropdown()
+
+ $issuableDueDate = $('#issuable-due-date')
+
+ if $issuableDueDate.length
+ $('.datepicker').datepicker(
+ dateFormat: 'yy-mm-dd',
+ onSelect: (dateText, inst) ->
+ $issuableDueDate.val dateText
+ ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())
initAutosave: ->
new Autosave @titleField, [
@@ -80,3 +90,23 @@ class @IssuableForm
addWip: ->
@titleField.val "WIP: #{@titleField.val()}"
+
+ initMoveDropdown: ->
+ $moveDropdown = $('.js-move-dropdown')
+
+ if $moveDropdown.length
+ $('.js-move-dropdown').select2
+ ajax:
+ url: $moveDropdown.data('projects-url')
+ results: (data) ->
+ return {
+ results: data
+ }
+ data: (query) ->
+ {
+ search: query
+ }
+ formatResult: (project) ->
+ project.name_with_namespace
+ formatSelection: (project) ->
+ project.name_with_namespace
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index 157361404e0..f446aa49cde 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -99,7 +99,7 @@ class @Issue
# If the user doesn't have the required permissions the container isn't
# rendered at all.
- return unless $container
+ return if $container.length is 0
$.getJSON($container.data('path'))
.error ->
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
index c5740f27ddd..ed50e2e698f 100644
--- a/app/assets/javascripts/issue_status_select.js.coffee
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -6,6 +6,13 @@ class @IssueStatusSelect
$(el).glDropdown(
selectable: true
fieldName: fieldName
+ toggleLabel: (selected, el, instance) =>
+ label = 'Author'
+ $item = instance.dropdown.find('.is-active')
+ label = $item.text() if $item.length
+ label
+ clicked: (item, $el, e)->
+ e.preventDefault()
id: (obj, el) ->
$(el).data("id")
)
diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee
new file mode 100644
index 00000000000..6b0e69dbae7
--- /dev/null
+++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee
@@ -0,0 +1,127 @@
+class @IssuableBulkActions
+ constructor: (opts = {}) ->
+ # Set defaults
+ {
+ @container = $('.content')
+ @form = @getElement('.bulk-update')
+ @issues = @getElement('.issues-list .issue')
+ } = opts
+
+ # Save instance
+ @form.data 'bulkActions', @
+
+ @willUpdateLabels = false
+
+ @bindEvents()
+
+ # Fixes bulk-assign not working when navigating through pages
+ Issuable.initChecks();
+
+ getElement: (selector) ->
+ @container.find selector
+
+ bindEvents: ->
+ @form.off('submit').on('submit', @onFormSubmit.bind(@))
+
+ onFormSubmit: (e) ->
+ e.preventDefault()
+ @submit()
+
+ submit: ->
+ _this = @
+
+ xhr = $.ajax
+ url: @form.attr 'action'
+ method: @form.attr 'method'
+ dataType: 'JSON',
+ data: @getFormDataAsObject()
+
+ xhr.done (response, status, xhr) ->
+ location.reload()
+
+ xhr.fail ->
+ new Flash("Issue update failed")
+
+ xhr.always @onFormSubmitAlways.bind(@)
+
+ onFormSubmitAlways: ->
+ @form.find('[type="submit"]').enable()
+
+ getSelectedIssues: ->
+ @issues.has('.selected_issue:checked')
+
+ getLabelsFromSelection: ->
+ labels = []
+
+ @getSelectedIssues().map ->
+ _labels = $(@).data('labels')
+ if _labels
+ _labels.map (labelId) ->
+ labels.push(labelId) if labels.indexOf(labelId) is -1
+
+ labels
+
+ ###*
+ * Will return only labels that were marked previously and the user has unmarked
+ * @return {Array} Label IDs
+ ###
+ getUnmarkedIndeterminedLabels: ->
+ result = []
+ labelsToKeep = []
+
+ for el in @getElement('.labels-filter .is-indeterminate')
+ labelsToKeep.push $(el).data('labelId')
+
+ for id in @getLabelsFromSelection()
+ # Only the ones that we are not going to keep
+ result.push(id) if labelsToKeep.indexOf(id) is -1
+
+ result
+
+ ###*
+ * Simple form serialization, it will return just what we need
+ * Returns key/value pairs from form data
+ ###
+ getFormDataAsObject: ->
+ formData =
+ update:
+ state_event : @form.find('input[name="update[state_event]"]').val()
+ assignee_id : @form.find('input[name="update[assignee_id]"]').val()
+ milestone_id : @form.find('input[name="update[milestone_id]"]').val()
+ issues_ids : @form.find('input[name="update[issues_ids]"]').val()
+ add_label_ids : []
+ remove_label_ids : []
+
+ if @willUpdateLabels
+ @getLabelsToApply().map (id) ->
+ formData.update.add_label_ids.push id
+
+ @getLabelsToRemove().map (id) ->
+ formData.update.remove_label_ids.push id
+
+ formData
+
+ getLabelsToApply: ->
+ labelIds = []
+ $labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
+
+ $labels.each (k, label) ->
+ labelIds.push parseInt($(label).val()) if label
+
+ labelIds
+
+ ###*
+ * Returns Label IDs that will be removed from issue selection
+ * @return {Array} Array of labels IDs
+ ###
+ getLabelsToRemove: ->
+ result = []
+ indeterminatedLabels = @getUnmarkedIndeterminedLabels()
+ labelsToApply = @getLabelsToApply()
+
+ indeterminatedLabels.map (id) ->
+ # We need to exclude label IDs that will be applied
+ # By not doing this will cause issues from selection to not add labels at all
+ result.push(id) if labelsToApply.indexOf(id) is -1
+
+ result
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
deleted file mode 100644
index 3330e6c68ad..00000000000
--- a/app/assets/javascripts/issues.js.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-@Issues =
- init: ->
- Issues.created = true
- Issues.initChecks()
-
- $("body").on "ajax:success", ".close_issue, .reopen_issue", ->
- t = $(this)
- totalIssues = undefined
- reopen = t.hasClass("reopen_issue")
- $(".issue_counter").each ->
- issue = $(this)
- totalIssues = parseInt($(this).html(), 10)
- if reopen and issue.closest(".main_menu").length
- $(this).html totalIssues + 1
- else
- $(this).html totalIssues - 1
-
- initChecks: ->
- $(".check_all_issues").click ->
- $(".selected_issue").prop("checked", @checked)
- Issues.checkChanged()
-
- $(".selected_issue").bind "change", Issues.checkChanged
-
- checkChanged: ->
- checked_issues = $(".selected_issue:checked")
- if checked_issues.length > 0
- ids = []
- $.each checked_issues, (index, value) ->
- ids.push $(value).attr("data-id")
-
- $("#update_issues_ids").val ids
- $(".issues-other-filters").hide()
- $(".issues_bulk_update").show()
- else
- $("#update_issues_ids").val []
- $(".issues_bulk_update").hide()
- $(".issues-other-filters").show()
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 995fd768603..7688609b301 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -1,5 +1,7 @@
class @LabelsSelect
constructor: ->
+ _this = @
+
$('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown)
projectId = $dropdown.data('project-id')
@@ -30,14 +32,14 @@ class @LabelsSelect
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
- <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%= _.escape(label.title) %>">
- <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>; color: <%= label.text_color %>;">
- <%= _.escape(label.title) %>
+ <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>">
+ <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
+ <%- label.title %>
</span>
</a>
<% }); %>'
)
- labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length
@@ -93,8 +95,11 @@ class @LabelsSelect
$newLabelCreateButton.enable()
if label.message?
+ errors = _.map label.message, (value, key) ->
+ "#{key} #{value[0]}"
+
$newLabelError
- .text label.message
+ .html errors.join("<br/>")
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
@@ -140,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
- template = labelNoneHTMLTemplate()
+ template = labelNoneHTMLTemplate
$value
.removeAttr('style')
.html(template)
@@ -196,10 +201,30 @@ class @LabelsSelect
callback data
- renderRow: (label) ->
- removesAll = label.id is 0 or not label.id?
+ renderRow: (label, instance) ->
+ $li = $('<li>')
+ $a = $('<a href="#">')
selectedClass = []
+ removesAll = label.id is 0 or not label.id?
+
+ if $dropdown.hasClass('js-filter-bulk-update')
+ indeterminate = instance.indeterminateIds
+ active = instance.activeIds
+
+ if indeterminate.indexOf(label.id) isnt -1
+ selectedClass.push 'is-indeterminate'
+
+ if active.indexOf(label.id) isnt -1
+ # Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf 'is-indeterminate'
+ selectedClass.splice i, 1 unless i is -1
+
+ selectedClass.push 'is-active'
+
+ # Add input manually
+ instance.addInput @fieldName, label.id
+
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -230,17 +255,21 @@ class @LabelsSelect
else
colorEl = ''
- "<li>
- <a href='#' class='#{selectedClass.join(' ')}'>
- #{colorEl}
- #{_.escape(label.title)}
- </a>
- </li>"
- filterable: true
+ # We need to identify which items are actually labels
+ if label.id
+ selectedClass.push('label-item')
+ $a.attr('data-label-id', label.id)
+
+ $a.addClass(selectedClass.join(' '))
+ .html("#{colorEl} #{label.title}")
+
+ # Return generated html
+ $li.html($a).prop('outerHTML')
+ persistWhenHide: $dropdown.data('persistWhenHide')
search:
fields: ['title']
selectable: true
-
+ filterable: true
toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
@@ -259,7 +288,7 @@ class @LabelsSelect
fieldName: $dropdown.data('field-name')
id: (label) ->
if $dropdown.hasClass("js-filter-submit") and not label.isAny?
- _.escape label.title
+ label.title
else
label.id
@@ -280,10 +309,21 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
- saveLabelData()
+ if not $dropdown.hasClass 'js-filter-bulk-update'
+ saveLabelData()
+
+ if $dropdown.hasClass('js-filter-bulk-update')
+ # If we are persisting state we need the classes
+ if not @options.persistWhenHide
+ $dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
+ _this.enableBulkLabelDropdown()
+
+ if $dropdown.hasClass('js-filter-bulk-update')
+ return
+
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
@@ -298,4 +338,49 @@ class @LabelsSelect
return
else
saveLabelData()
+
+ setIndeterminateIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @indeterminateIds = _this.getIndeterminateIds()
+
+ setActiveIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @activeIds = _this.getActiveIds()
)
+
+ @bindEvents()
+
+ bindEvents: ->
+ $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
+
+ onSelectCheckboxIssue: ->
+ return if $('.selected_issue:checked').length
+
+ # Remove inputs
+ $('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
+
+ # Also restore button text
+ $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
+
+ getIndeterminateIds: ->
+ label_ids = []
+
+ $('.selected_issue:checked').each (i, el) ->
+ issue_id = $(el).data('id')
+ label_ids.push $("#issue_#{issue_id}").data('labels')
+
+ _.flatten(label_ids)
+
+ getActiveIds: ->
+ label_ids = []
+
+ $('.selected_issue:checked').each (i, el) ->
+ issue_id = $(el).data('id')
+ label_ids.push $("#issue_#{issue_id}").data('labels')
+
+ _.intersection.apply _, label_ids
+
+ enableBulkLabelDropdown: ->
+ if $('.selected_issue:checked').length
+ issuableBulkActions = $('.bulk-update').data('bulkActions')
+ issuableBulkActions.willUpdateLabels = true
diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee
new file mode 100644
index 00000000000..f639f7f5892
--- /dev/null
+++ b/app/assets/javascripts/layout_nav.js.coffee
@@ -0,0 +1,24 @@
+hideEndFade = ($scrollingTabs) ->
+ $scrollingTabs.each ->
+ $this = $(@)
+
+ $this
+ .siblings('.fade-right')
+ .toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'))
+
+$ ->
+
+ hideEndFade($('.scrolling-tabs'))
+
+ $(window)
+ .off 'resize.nav'
+ .on 'resize.nav', ->
+ hideEndFade($('.scrolling-tabs'))
+
+ $('.scrolling-tabs').on 'scroll', (event) ->
+ $this = $(this)
+ currentPosition = $this.scrollLeft()
+ maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
+
+ $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0)
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1)
diff --git a/app/assets/javascripts/lib/chart.js.coffee b/app/assets/javascripts/lib/chart.js.coffee
new file mode 100644
index 00000000000..82217fc5107
--- /dev/null
+++ b/app/assets/javascripts/lib/chart.js.coffee
@@ -0,0 +1 @@
+#= require Chart
diff --git a/app/assets/javascripts/lib/d3.js.coffee b/app/assets/javascripts/lib/d3.js.coffee
new file mode 100644
index 00000000000..74f0a0bb06a
--- /dev/null
+++ b/app/assets/javascripts/lib/d3.js.coffee
@@ -0,0 +1 @@
+#= require d3
diff --git a/app/assets/javascripts/lib/raphael.js.coffee b/app/assets/javascripts/lib/raphael.js.coffee
new file mode 100644
index 00000000000..ab8e5979b87
--- /dev/null
+++ b/app/assets/javascripts/lib/raphael.js.coffee
@@ -0,0 +1,3 @@
+#= require raphael
+#= require g.raphael
+#= require g.bar
diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/utils/animate.js.coffee
index ec3b44d6126..ec3b44d6126 100644
--- a/app/assets/javascripts/lib/animate.js.coffee
+++ b/app/assets/javascripts/lib/utils/animate.js.coffee
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee
new file mode 100644
index 00000000000..e39dcb2daa9
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/common_utils.js.coffee
@@ -0,0 +1,65 @@
+((w) ->
+
+ w.gl or= {}
+ w.gl.utils or= {}
+
+ w.gl.utils.isInGroupsPage = ->
+
+ return $('body').data('page').split(':')[0] is 'groups'
+
+
+ w.gl.utils.isInProjectPage = ->
+
+ return $('body').data('page').split(':')[0] is 'projects'
+
+
+ w.gl.utils.getProjectSlug = ->
+
+ return if @isInProjectPage() then $('body').data 'project' else null
+
+
+ w.gl.utils.getGroupSlug = ->
+
+ return if @isInGroupsPage() then $('body').data 'group' else null
+
+
+
+ gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
+
+ $tooltipEl
+ .tooltip 'destroy'
+ .attr 'title', newTitle
+ .tooltip 'fixTitle'
+
+
+ gl.utils.preventDisabledButtons = ->
+
+ $('.btn').click (e) ->
+ if $(this).hasClass 'disabled'
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ return false
+
+
+ jQuery.timefor = (time, suffix, expiredLabel) ->
+
+ return '' unless time
+
+ suffix or= 'remaining'
+ expiredLabel or= 'Past due'
+
+ jQuery.timeago.settings.allowFuture = yes
+
+ { suffixFromNow } = jQuery.timeago.settings.strings
+ jQuery.timeago.settings.strings.suffixFromNow = suffix
+
+ timefor = $.timeago time
+
+ if timefor.indexOf('ago') > -1
+ timefor = expiredLabel
+
+ jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
+
+ return timefor
+
+) window
diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee
index ad1d1c70481..948d6dbf07e 100644
--- a/app/assets/javascripts/lib/datetime_utility.js.coffee
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee
@@ -12,6 +12,13 @@
$el.attr('title', gl.utils.formatDate($el.attr('datetime')))
)
- $timeagoEls.timeago() if setTimeago
+ if setTimeago
+ $timeagoEls.timeago()
+ $timeagoEls.tooltip('destroy')
+
+ # Recreate with custom template
+ $timeagoEls.tooltip(
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ )
) window
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
new file mode 100644
index 00000000000..80f9936b9c2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
@@ -0,0 +1,2 @@
+gl.emojiAliases = ->
+ JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
diff --git a/app/assets/javascripts/lib/jquery.timeago.js b/app/assets/javascripts/lib/utils/jquery.timeago.js
index cc17aa7d3d1..cc17aa7d3d1 100644
--- a/app/assets/javascripts/lib/jquery.timeago.js
+++ b/app/assets/javascripts/lib/utils/jquery.timeago.js
diff --git a/app/assets/javascripts/lib/md5.js b/app/assets/javascripts/lib/utils/md5.js
index b63716eaad2..b63716eaad2 100644
--- a/app/assets/javascripts/lib/md5.js
+++ b/app/assets/javascripts/lib/utils/md5.js
diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/utils/notify.js.coffee
index 9e28353ac34..9e28353ac34 100644
--- a/app/assets/javascripts/lib/notify.js.coffee
+++ b/app/assets/javascripts/lib/utils/notify.js.coffee
diff --git a/app/assets/javascripts/lib/utils/text_utility.js.coffee b/app/assets/javascripts/lib/utils/text_utility.js.coffee
new file mode 100644
index 00000000000..2e1407f8738
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_utility.js.coffee
@@ -0,0 +1,105 @@
+((w) ->
+ w.gl ?= {}
+ w.gl.text ?= {}
+
+ gl.text.randomString = -> Math.random().toString(36).substring(7)
+
+ gl.text.replaceRange = (s, start, end, substitute) ->
+ s.substring(0, start) + substitute + s.substring(end);
+
+ gl.text.selectedText = (text, textarea) ->
+ text.substring(textarea.selectionStart, textarea.selectionEnd)
+
+ gl.text.lineBefore = (text, textarea) ->
+ split = text.substring(0, textarea.selectionStart).trim().split('\n')
+ split[split.length - 1]
+
+ gl.text.lineAfter = (text, textarea) ->
+ text.substring(textarea.selectionEnd).trim().split('\n')[0]
+
+ gl.text.blockTagText = (text, textArea, blockTag, selected) ->
+ lineBefore = @lineBefore(text, textArea)
+ lineAfter = @lineAfter(text, textArea)
+
+ if lineBefore is blockTag and lineAfter is blockTag
+ # To remove the block tag we have to select the line before & after
+ if blockTag?
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1)
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1)
+
+ selected
+ else
+ "#{blockTag}\n#{selected}\n#{blockTag}"
+
+ gl.text.insertText = (textArea, text, tag, blockTag, selected, wrap) ->
+ selectedSplit = selected.split('\n')
+ startChar = if not wrap and textArea.selectionStart > 0 then '\n' else ''
+
+ if selectedSplit.length > 1 and (not wrap or blockTag?)
+ if blockTag?
+ insertText = @blockTagText(text, textArea, blockTag, selected)
+ else
+ insertText = selectedSplit.map((val) ->
+ if val.indexOf(tag) is 0
+ "#{val.replace(tag, '')}"
+ else
+ "#{tag}#{val}"
+ ).join('\n')
+ else
+ insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}"
+
+ if document.queryCommandSupported('insertText')
+ inserted = document.execCommand 'insertText', false, insertText
+
+ unless inserted
+ try
+ document.execCommand("ms-beginUndoUnit")
+
+ textArea.value = @replaceRange(
+ text,
+ textArea.selectionStart,
+ textArea.selectionEnd,
+ insertText)
+ try
+ document.execCommand("ms-endUndoUnit")
+
+ @moveCursor(textArea, tag, wrap)
+
+ gl.text.moveCursor = (textArea, tag, wrapped) ->
+ return unless textArea.setSelectionRange
+
+ if textArea.selectionStart is textArea.selectionEnd
+ if wrapped
+ pos = textArea.selectionStart - tag.length
+ else
+ pos = textArea.selectionStart
+
+ textArea.setSelectionRange pos, pos
+
+ gl.text.updateText = (textArea, tag, blockTag, wrap) ->
+ $textArea = $(textArea)
+ oldVal = $textArea.val()
+ textArea = $textArea.get(0)
+ text = $textArea.val()
+ selected = @selectedText(text, textArea)
+ $textArea.focus()
+
+ @insertText(textArea, text, tag, blockTag, selected, wrap)
+
+ gl.text.init = (form) ->
+ self = @
+ $('.js-md', form)
+ .off 'click'
+ .on 'click', ->
+ $this = $(@)
+ self.updateText(
+ $this.closest('.md-area').find('textarea'),
+ $this.data('md-tag'),
+ $this.data('md-block'),
+ not $this.data('md-prepend')
+ )
+
+ gl.text.removeListeners = (form) ->
+ $('.js-md', form).off()
+
+) window
diff --git a/app/assets/javascripts/lib/utils/type_utility.js.coffee b/app/assets/javascripts/lib/utils/type_utility.js.coffee
new file mode 100644
index 00000000000..957f0d86b36
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/type_utility.js.coffee
@@ -0,0 +1,9 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.utils ?= {}
+
+ w.gl.utils.isObject = (obj) ->
+ obj? and (obj.constructor is Object)
+
+) window
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/utils/url_utility.js.coffee
index 6a00932c028..e8085e1c2e4 100644
--- a/app/assets/javascripts/lib/url_utility.js.coffee
+++ b/app/assets/javascripts/lib/utils/url_utility.js.coffee
@@ -26,10 +26,19 @@
newUrl = decodeURIComponent(url)
for paramName, paramValue of params
pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
- if url.search(pattern) >= 0
+ if not paramValue?
+ newUrl = newUrl.replace pattern, ''
+ else if url.search(pattern) isnt -1
newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
else
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
+
+ # Remove a trailing ampersand
+ lastChar = newUrl[newUrl.length - 1]
+
+ if lastChar is '&'
+ newUrl = newUrl.slice 0, -1
+
newUrl
# removes parameter query string from url. returns the modified url
diff --git a/app/assets/javascripts/lib/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js
index 39ffe44dae0..39ffe44dae0 100644
--- a/app/assets/javascripts/lib/utf8_encode.js
+++ b/app/assets/javascripts/lib/utils/utf8_encode.js
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
index d14b7139237..dc2590a0355 100644
--- a/app/assets/javascripts/logo.js.coffee
+++ b/app/assets/javascripts/logo.js.coffee
@@ -42,9 +42,3 @@ work = ->
$(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/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 1f46e331427..dabfd91cf14 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -9,7 +9,7 @@ class @MergeRequest
# Options:
# action - String, current controller action
#
- constructor: (@opts) ->
+ constructor: (@opts = {}) ->
this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', =>
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 372732d0aac..894f80586f1 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -75,6 +75,9 @@ class @MergeRequestTabs
@loadDiff($target.attr('href'))
if bp? and bp.getBreakpointSize() isnt 'lg'
@shrinkView()
+
+ navBarHeight = $('.navbar-gitlab').outerHeight()
+ $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight)
else if action == 'builds'
@loadBuilds($target.attr('href'))
@expandView()
@@ -85,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
- navBarHeight = $('.navbar-gitlab').outerHeight()
+ navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index f58647988a2..779f536d9f0 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -10,6 +10,7 @@ class @MergeRequestWidget
$('#modal_merge_info').modal(show: false)
@firstCICheck = true
@readyForCICheck = false
+ @cancel = false
clearInterval @fetchBuildStatusInterval
@clearEventListeners()
@@ -21,10 +22,16 @@ class @MergeRequestWidget
clearEventListeners: ->
$(document).off 'page:change.merge_request'
+ cancelPolling: ->
+ @cancel = true
+
addEventListeners: ->
+ allowedPages = ['show', 'commits', 'builds', 'changes']
$(document).on 'page:change.merge_request', =>
- if $('body').data('page') isnt 'projects:merge_requests:show'
+ page = $('body').data('page').split(':').last()
+ if allowedPages.indexOf(page) < 0
clearInterval @fetchBuildStatusInterval
+ @cancelPolling()
@clearEventListeners()
mergeInProgress: (deleteSourceBranch = false)->
@@ -67,6 +74,7 @@ class @MergeRequestWidget
$('.ci-widget-fetching').show()
$.getJSON @opts.ci_status_url, (data) =>
+ return if @cancel
@readyForCICheck = true
if data.status is ''
@@ -106,6 +114,7 @@ class @MergeRequestWidget
@firstCICheck = false
showCIStatus: (state) ->
+ return if not state?
$('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"]
if state in allowed_states
@@ -113,7 +122,7 @@ class @MergeRequestWidget
switch state
when "failed", "canceled", "not_found"
@setMergeButtonClass('btn-danger')
- when "running", "pending"
+ when "running"
@setMergeButtonClass('btn-warning')
when "success"
@setMergeButtonClass('btn-create')
@@ -126,6 +135,6 @@ class @MergeRequestWidget
$('.ci_widget:visible .ci-coverage').text(text)
setMergeButtonClass: (css_class) ->
- $('.accept_merge_request')
+ $('.js-merge-button,.accept-action .dropdown-toggle')
.removeClass('btn-danger btn-warning btn-create')
.addClass(css_class)
diff --git a/app/assets/javascripts/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee
new file mode 100644
index 00000000000..4929295c10b
--- /dev/null
+++ b/app/assets/javascripts/merged_buttons.js.coffee
@@ -0,0 +1,30 @@
+class @MergedButtons
+ constructor: ->
+ @$removeBranchWidget = $('.remove_source_branch_widget')
+ @$removeBranchProgress = $('.remove_source_branch_in_progress')
+ @$removeBranchFailed = $('.remove_source_branch_widget.failed')
+
+ @cleanEventListeners()
+ @initEventListeners()
+
+ cleanEventListeners: ->
+ $(document).off 'click', '.remove_source_branch'
+ $(document).off 'ajax:success', '.remove_source_branch'
+ $(document).off 'ajax:error', '.remove_source_branch'
+
+ initEventListeners: ->
+ $(document).on 'click', '.remove_source_branch', @removeSourceBranch
+ $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
+ $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
+
+ removeSourceBranch: =>
+ @$removeBranchWidget.hide()
+ @$removeBranchProgress.show()
+
+ removeBranchSuccess: ->
+ location.reload()
+
+ removeBranchError: ->
+ @$removeBranchWidget.hide()
+ @$removeBranchProgress.hide()
+ @$removeBranchFailed.show()
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index 0037a3a21c2..a19e68b39e2 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -4,18 +4,10 @@ class @Milestone
type: "PUT"
url: issue_url
data: data
- success: (data) ->
- if data.saved == true
- if data.assignee_avatar_url
- img_tag = $('<img/>')
- img_tag.attr('src', data.assignee_avatar_url)
- img_tag.addClass('avatar s16')
- $(li).find('.assignee-icon').html(img_tag)
- else
- $(li).find('.assignee-icon').html('')
- $(li).effect 'highlight'
- else
- new Flash("Issue update failed", 'alert')
+ success: (_data) =>
+ @successCallback(_data, li)
+ error: (data) ->
+ new Flash("Issue update failed", 'alert')
dataType: "json"
@sortIssues: (data) ->
@@ -25,9 +17,10 @@ class @Milestone
type: "PUT"
url: sort_issues_url
data: data
- success: (data) ->
- if data.saved != true
- new Flash("Issues update failed", 'alert')
+ success: (_data) =>
+ @successCallback(_data)
+ error: ->
+ new Flash("Issues update failed", 'alert')
dataType: "json"
@sortMergeRequests: (data) ->
@@ -37,9 +30,10 @@ class @Milestone
type: "PUT"
url: sort_mr_url
data: data
- success: (data) ->
- if data.saved != true
- new Flash("MR update failed", 'alert')
+ success: (_data) =>
+ @successCallback(_data)
+ error: (data) ->
+ new Flash("Issue update failed", 'alert')
dataType: "json"
@updateMergeRequest: (li, merge_request_url, data) ->
@@ -47,20 +41,23 @@ class @Milestone
type: "PUT"
url: merge_request_url
data: data
- success: (data) ->
- if data.saved == true
- if data.assignee_avatar_url
- img_tag = $('<img/>')
- img_tag.attr('src', data.assignee_avatar_url)
- img_tag.addClass('avatar s16')
- $(li).find('.assignee-icon').html(img_tag)
- else
- $(li).find('.assignee-icon').html('')
- $(li).effect 'highlight'
- else
- new Flash("Issue update failed", 'alert')
+ success: (_data) =>
+ @successCallback(_data, li)
+ error: (data) ->
+ new Flash("Issue update failed", 'alert')
dataType: "json"
+ @successCallback: (data, element) =>
+ if data.assignee
+ img_tag = $('<img/>')
+ img_tag.attr('src', data.assignee.avatar_url)
+ img_tag.addClass('avatar s16')
+ $(element).find('.assignee-icon').html(img_tag)
+ else
+ $(element).find('.assignee-icon').html('')
+
+ $(element).effect 'highlight'
+
constructor: ->
oldMouseStart = $.ui.sortable.prototype._mouseStart
$.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
@@ -81,8 +78,10 @@ class @Milestone
stop: (event, ui) ->
$(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
- data = $(this).sortable("serialize")
- Milestone.sortIssues(data)
+ # Prevents sorting from container which element has been removed.
+ if $(this).find(ui.item).length > 0
+ data = $(this).sortable("serialize")
+ Milestone.sortIssues(data)
receive: (event, ui) ->
new_state = $(this).data('state')
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 345a0e447af..8ab03ed93ee 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -24,10 +24,16 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
- '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>'
+ '<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'
)
- milestoneLinkNoneTemplate = '<div class="light">None</div>'
+ milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
+
+ collapsedSidebarLabelTemplate = _.template(
+ '<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left">
+ <%- title %>
+ </span>'
+ )
$dropdown.glDropdown(
data: (term, callback) ->
@@ -83,7 +89,7 @@ class @MilestoneSelect
$selectbox.hide()
# display:block overrides the hide-collapse rule
- $value.removeAttr('style')
+ $value.css('display', '')
clicked: (selected) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
@@ -106,7 +112,7 @@ class @MilestoneSelect
.val()
data = {}
data[abilityName] = {}
- data[abilityName].milestone_id = selected
+ data[abilityName].milestone_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
@@ -118,12 +124,13 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
- $value.removeAttr('style')
+ $value.css('display', '')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
+ data.milestone.remaining = $.timefor data.milestone.due_date
$value.html(milestoneLinkTemplate(data.milestone))
- $sidebarCollapsedValue.find('span').text(data.milestone.title)
+ $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone))
else
$value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No')
diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee
index a02c4515ccc..3b419dff105 100644
--- a/app/assets/javascripts/namespace_select.js.coffee
+++ b/app/assets/javascripts/namespace_select.js.coffee
@@ -1,25 +1,56 @@
class @NamespaceSelect
- constructor: ->
- namespaceFormatResult = (namespace) ->
- markup = "<div class='namespace-result'>"
- markup += "<span class='namespace-kind'>" + namespace.kind + "</span>"
- markup += "<span class='namespace-path'>" + namespace.path + "</span>"
- markup += "</div>"
- markup
-
- formatSelection = (namespace) ->
- namespace.kind + ": " + namespace.path
-
- $('.ajax-namespace-select').each (i, select) ->
- $(select).select2
- placeholder: "Search for namespace"
- multiple: $(select).hasClass('multiselect')
- minimumInputLength: 0
- query: (query) ->
- Api.namespaces query.term, (namespaces) ->
- data = { results: namespaces }
- query.callback(data)
-
- dropdownCssClass: "ajax-namespace-dropdown"
- formatResult: namespaceFormatResult
- formatSelection: formatSelection
+ constructor: (opts) ->
+ {
+ @dropdown
+ } = opts
+
+ showAny = true
+ fieldName = 'namespace_id'
+
+ if @dropdown.attr 'data-field-name'
+ fieldName = @dropdown.data 'fieldName'
+
+ if @dropdown.attr 'data-show-any'
+ showAny = @dropdown.data 'showAny'
+
+ @dropdown.glDropdown(
+ filterable: true
+ selectable: true
+ filterRemote: true
+ search:
+ fields: ['path']
+ fieldName: fieldName
+ toggleLabel: (selected) ->
+ return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}"
+ data: (term, dataCallback) ->
+ Api.namespaces term, (namespaces) ->
+ if showAny
+ anyNamespace =
+ text: 'Any namespace'
+ id: null
+
+ namespaces.unshift(anyNamespace)
+ namespaces.splice 1, 0, 'divider'
+
+ dataCallback(namespaces)
+ text: (namespace) ->
+ return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}"
+ renderRow: @renderRow
+ clicked: @onSelectItem
+ )
+
+ onSelectItem: (item, el, e) =>
+ e.preventDefault()
+
+class @NamespaceSelects
+ constructor: (opts = {}) ->
+ {
+ @$dropdowns = $('.js-namespace-select')
+ } = opts
+
+ @$dropdowns.each (i, dropdown) ->
+ $dropdown = $(dropdown)
+
+ new NamespaceSelect(
+ dropdown: $dropdown
+ )
diff --git a/app/assets/javascripts/network/application.js.coffee b/app/assets/javascripts/network/application.js.coffee
new file mode 100644
index 00000000000..f75f63869c5
--- /dev/null
+++ b/app/assets/javascripts/network/application.js.coffee
@@ -0,0 +1,17 @@
+# This is a manifest file that'll be compiled into including all the files listed below.
+# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+# be included in the compiled file accessible from http://example.com/assets/application.js
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+#= require_tree .
+
+$ ->
+ network_graph = new Network({
+ url: $(".network-graph").attr('data-url'),
+ commit_url: $(".network-graph").attr('data-commit-url'),
+ ref: $(".network-graph").attr('data-ref'),
+ commit_id: $(".network-graph").attr('data-commit-id')
+ })
+
+ new ShortcutsNetwork(network_graph.branch_graph)
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee
index f2fd2a775a4..f2fd2a775a4 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/network/branch-graph.js.coffee
diff --git a/app/assets/javascripts/network.js.coffee b/app/assets/javascripts/network/network.js.coffee
index f4ef07a50a7..f4ef07a50a7 100644
--- a/app/assets/javascripts/network.js.coffee
+++ b/app/assets/javascripts/network/network.js.coffee
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index efb3e8e2198..0b7d8f64456 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -100,13 +100,43 @@ class @Notes
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
- keydownNoteText: (e) ->
- $this = $(this)
- if $this.val() is '' and e.which is 38 #aka the up key
- myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
- if myLastNote.length
- myLastNoteEditBtn = myLastNote.find('.js-note-edit')
- myLastNoteEditBtn.trigger('click', [true, myLastNote])
+ keydownNoteText: (e) =>
+ return if isMetaKey e
+
+ $textarea = $(e.target)
+
+ # Edit previous note when UP arrow is hit
+ switch e.which
+ when 38
+ return unless $textarea.val() is ''
+
+ myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
+ if myLastNote.length
+ myLastNoteEditBtn = myLastNote.find('.js-note-edit')
+ myLastNoteEditBtn.trigger('click', [true, myLastNote])
+
+ # Cancel creating diff note or editing any note when ESCAPE is hit
+ when 27
+ discussionNoteForm = $textarea.closest('.js-discussion-note-form')
+ if discussionNoteForm.length
+ if $textarea.val() isnt ''
+ return unless confirm('Are you sure you want to cancel creating this comment?')
+
+ @removeDiscussionNoteForm(discussionNoteForm)
+ return
+
+ editNote = $textarea.closest('.note')
+ if editNote.length
+ originalText = $textarea.closest('form').data('original-note')
+ newText = $textarea.val()
+ if originalText isnt newText
+ return unless confirm('Are you sure you want to cancel editing this comment?')
+
+ @removeNoteEditForm(editNote)
+
+
+ isMetaKey = (e) ->
+ (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
initRefresh: ->
clearInterval(Notes.interval)
@@ -114,13 +144,15 @@ class @Notes
@refresh()
, @pollingInterval
- refresh: ->
- return if @refreshing is true
- refreshing = true
+ refresh: =>
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
+ return if @refreshing
+
+ @refreshing = true
+
$.ajax
url: @notes_url
data: "last_fetched_at=" + @last_fetched_at
@@ -134,8 +166,8 @@ class @Notes
@renderDiscussionNote(note)
else
@renderNote(note)
- always: =>
- @refreshing = false
+ .always () =>
+ @refreshing = false
###
Increase @pollingInterval up to 120 seconds on every function call,
@@ -162,13 +194,14 @@ class @Notes
renderNote: (note) ->
unless note.valid
if note.award
- flash = new Flash('You have already used this award emoji!', 'alert')
+ flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content')
return
if note.award
- awardsHandler.addAwardToEmojiBar(note.note)
- awardsHandler.scrollToAwards()
+ votesBlock = $('.js-awards-block').eq 0
+ gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
+ gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list
# or skip if rendered
@@ -207,12 +240,16 @@ class @Notes
@note_ids.push(note.id)
form = $("#new-discussion-note-form-#{note.discussion_id}")
+ if note.original_discussion_id? and form.length is 0
+ form = $("#new-discussion-note-form-#{note.original_discussion_id}")
row = form.closest("tr")
note_html = $(note.html)
note_html.syntaxHighlight()
# is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
+ if note.original_discussion_id? and discussionContainer.length is 0
+ discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']")
if discussionContainer.length is 0
# insert the note and the reply button after the temp row
row.after note.discussion_html
@@ -285,6 +322,8 @@ class @Notes
form.addClass "js-main-target-form"
form.find("#note_line_code").remove()
+ form.find("#note_position").remove()
+ form.find("#note_type").remove()
###
General note form setup.
@@ -301,10 +340,12 @@ class @Notes
new Autosave textarea, [
"Note"
- form.find("#note_commit_id").val()
- form.find("#note_line_code").val()
form.find("#note_noteable_type").val()
form.find("#note_noteable_id").val()
+ form.find("#note_commit_id").val()
+ form.find("#note_type").val()
+ form.find("#note_line_code").val()
+ form.find("#note_position").val()
]
###
@@ -328,7 +369,7 @@ class @Notes
@renderDiscussionNote(note)
# cleanup after successfully creating a diff/discussion note
- @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
+ @removeDiscussionNoteForm($(xhr.target))
###
Called in response to the edit note form being submitted
@@ -352,8 +393,7 @@ class @Notes
Called in response to clicking the edit note link
Replaces the note text with the note edit form
- Adds a hidden div with the original content of the note to fill the edit note form with
- if the user cancels
+ Adds a data attribute to the form with the original content of the note for cancellations
###
showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault()
@@ -369,6 +409,8 @@ class @Notes
done = ($noteText) ->
# Neat little trick to put the cursor at the end
noteTextVal = $noteText.val()
+ # Store the original note text in a data attribute to retrieve if a user cancels edit.
+ form.find('form.edit-note').data 'original-note', noteTextVal
$noteText.val('').val(noteTextVal);
new GLForm form
@@ -391,14 +433,19 @@ class @Notes
###
Called in response to clicking the edit note link
- Hides edit form
+ Hides edit form and restores the original note text to the editor textarea.
###
- cancelEdit: (e) ->
+ cancelEdit: (e) =>
e.preventDefault()
- note = $(this).closest(".note")
+ note = $(e.target).closest('.note')
+ @removeNoteEditForm(note)
+
+ removeNoteEditForm: (note) ->
+ form = note.find(".current-note-edit-form")
note.removeClass "is-editting"
- note.find(".current-note-edit-form")
- .removeClass("current-note-edit-form")
+ form.removeClass("current-note-edit-form")
+ # Replace markdown textarea text with original note text.
+ form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'))
###
Called in response to deleting a note of any kind.
@@ -472,9 +519,12 @@ class @Notes
setupDiscussionNoteForm: (dataHolder, form) =>
# setup note target
form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
+ form.attr "data-line-code", dataHolder.data("lineCode")
+ form.find("#note_type").val dataHolder.data("noteType")
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_position").val dataHolder.attr("data-position")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee
new file mode 100644
index 00000000000..0bbd082c156
--- /dev/null
+++ b/app/assets/javascripts/notifications_dropdown.js.coffee
@@ -0,0 +1,25 @@
+class @NotificationsDropdown
+ constructor: ->
+ $(document)
+ .off 'click', '.update-notification'
+ .on 'click', '.update-notification', (e) ->
+ e.preventDefault()
+
+ return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
+
+ notificationLevel = $(@).data 'notification-level'
+ label = $(@).data 'notification-title'
+ form = $(this).parents('.notification-form:first')
+ form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
+ form.find('#notification_setting_level').val(notificationLevel)
+ form.submit()
+
+ $(document)
+ .off 'ajax:success', '.notification-form'
+ .on 'ajax:success', '.notification-form', (e, data) ->
+ if data.saved
+ $(e.currentTarget)
+ .closest('.notification-dropdown')
+ .replaceWith(data.html)
+ else
+ new Flash('Failed to save new settings', 'alert')
diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee
new file mode 100644
index 00000000000..3432428702a
--- /dev/null
+++ b/app/assets/javascripts/notifications_form.js.coffee
@@ -0,0 +1,49 @@
+class @NotificationsForm
+ constructor: ->
+ @removeEventListeners()
+ @initEventListeners()
+
+ removeEventListeners: ->
+ $(document).off 'change', '.js-custom-notification-event'
+
+ initEventListeners: ->
+ $(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
+
+ toggleCheckbox: (e) =>
+ $checkbox = $(e.currentTarget)
+ $parent = $checkbox.closest('.checkbox')
+ @saveEvent($checkbox, $parent)
+
+ showCheckboxLoadingSpinner: ($parent) ->
+ $parent
+ .addClass 'is-loading'
+ .find '.custom-notification-event-loading'
+ .removeClass 'fa-check'
+ .addClass 'fa-spin fa-spinner'
+ .removeClass 'is-done'
+
+ saveEvent: ($checkbox, $parent) ->
+ form = $parent.parents('form:first')
+
+ $.ajax(
+ url: form.attr('action')
+ method: form.attr('method')
+ dataType: 'json'
+ data: form.serialize()
+
+ beforeSend: =>
+ @showCheckboxLoadingSpinner($parent)
+ ).done (data) ->
+ $checkbox.enable()
+
+ if data.saved
+ $parent
+ .find '.custom-notification-event-loading'
+ .toggleClass 'fa-spin fa-spinner fa-check is-done'
+
+ setTimeout(->
+ $parent
+ .removeClass 'is-loading'
+ .find '.custom-notification-event-loading'
+ .toggleClass 'fa-spin fa-spinner fa-check is-done'
+ , 2000)
diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee
index 0ff83b7f0c8..8049c5c30e2 100644
--- a/app/assets/javascripts/pager.js.coffee
+++ b/app/assets/javascripts/pager.js.coffee
@@ -1,5 +1,5 @@
@Pager =
- init: (@limit = 0, preload, @disable = false) ->
+ init: (@limit = 0, preload, @disable = false, @callback = $.noop) ->
@loading = $('.loading').first()
if preload
@@ -19,6 +19,7 @@
@loading.hide()
success: (data) ->
Pager.append(data.count, data.html)
+ Pager.callback()
dataType: "json"
append: (count, html) ->
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 26a12423521..1583d1ba6f9 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
+ # Automatically submit email form when it changes
+ $('#user_notification_email').on 'change', ->
+ $(this).parents('form').submit()
+
$('.update-username').on 'ajax:before', ->
$('.loading-username').show()
$(this).find('.update-success').hide()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 07be85a32a5..3288c801388 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -19,6 +19,7 @@ class @Project
$('.clone').text(url)
# Ref switcher
+ @initRefSwitcher()
$('.project-refs-select').on 'change', ->
$(@).parents('form').submit()
@@ -34,23 +35,6 @@ class @Project
$(@).parents('.no-password-message').remove()
e.preventDefault()
- $('.update-notification').on 'click', (e) ->
- e.preventDefault()
- notification_level = $(@).data 'notification-level'
- label = $(@).data 'notification-title'
- $('#notification_setting_level').val(notification_level)
- $('#notification-form').submit()
- $('#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'
-
- $('#notification-form').on 'ajax:success', (e, data) ->
- if data.saved
- new Flash("Notification settings saved", "notice")
- else
- new Flash("Failed to save new settings", "alert")
-
-
@projectSelectDropdown()
projectSelectDropdown: ->
@@ -66,3 +50,42 @@ class @Project
changeProject: (url) ->
window.location = url
+
+ initRefSwitcher: ->
+ $('.js-project-refs-dropdown').each ->
+ $dropdown = $(@)
+ selected = $dropdown.data('selected')
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: $dropdown.data('refs-url')
+ data:
+ ref: $dropdown.data('ref')
+ ).done (refs) ->
+ callback(refs)
+ selectable: true
+ filterable: true
+ filterByText: true
+ fieldName: 'ref'
+ renderRow: (ref) ->
+ if ref.header?
+ $('<li />')
+ .addClass('dropdown-header')
+ .text(ref.header)
+ else
+ link = $('<a />')
+ .attr('href', '#')
+ .addClass(if ref is selected then 'is-active' else '')
+ .text(ref)
+ .attr('data-ref', escape(ref))
+
+ $('<li />')
+ .append(link)
+ id: (obj, $el) ->
+ $el.attr('data-ref')
+ toggleLabel: (obj, $el) ->
+ $el.text().trim()
+ clicked: (e) ->
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index 63dee4ed5d7..e48343a19b5 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -7,12 +7,17 @@ class @ProjectNew
@toggleSettingsOnclick()
- toggleSettings: ->
- checked = $("#project_builds_enabled").prop("checked")
- if checked
- $('.builds-feature').show()
- else
- $('.builds-feature').hide()
+ toggleSettings: =>
+ @_showOrHide('#project_builds_enabled', '.builds-feature')
+ @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
toggleSettingsOnclick: ->
- $("#project_builds_enabled").on 'click', @toggleSettings
+ $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings
+
+ _showOrHide: (checkElement, container) ->
+ $container = $(container)
+
+ if $(checkElement).prop('checked')
+ $container.show()
+ else
+ $container.hide()
diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee
new file mode 100644
index 00000000000..6d45770ace9
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_select.js.coffee
@@ -0,0 +1,40 @@
+class @ProtectedBranchSelect
+ constructor: (currentProject) ->
+ $('.dropdown-footer').hide();
+ @dropdown = $('.js-protected-branch-select').glDropdown(
+ data: @getProtectedBranches
+ filterable: true
+ remote: false
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch'
+ fieldName: 'protected_branch[name]'
+ text: (protected_branch) -> _.escape(protected_branch.title)
+ id: (protected_branch) -> _.escape(protected_branch.id)
+ onFilter: @toggleCreateNewButton
+ clicked: () -> $('.protect-branch-btn').attr('disabled', false)
+ )
+
+ $('.create-new-protected-branch').on 'click', (event) =>
+ # Refresh the dropdown's data, which ends up calling `getProtectedBranches`
+ @dropdown.data('glDropdown').remote.execute()
+ @dropdown.data('glDropdown').selectRowAtIndex(event, 0)
+
+ getProtectedBranches: (term, callback) =>
+ if @selectedBranch
+ callback(gon.open_branches.concat(@selectedBranch))
+ else
+ callback(gon.open_branches)
+
+ toggleCreateNewButton: (branchName) =>
+ @selectedBranch = { title: branchName, id: branchName, text: branchName }
+
+ if branchName is ''
+ $('.protected-branch-select-footer-list').addClass('hidden')
+ $('.dropdown-footer').hide();
+ else
+ $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}")
+ $('.protected-branch-select-footer-list').removeClass('hidden')
+ $('.dropdown-footer').show();
+
diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee
index 5753c9d4e72..79c2306e4d2 100644
--- a/app/assets/javascripts/protected_branches.js.coffee
+++ b/app/assets/javascripts/protected_branches.js.coffee
@@ -11,7 +11,8 @@ $ ->
dataType: "json"
data:
id: id
- developers_can_push: checked
+ protected_branch:
+ developers_can_push: checked
success: ->
row = $(e.target)
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
index 2d084b76cfe..12340bbce54 100644
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -10,6 +10,93 @@ class @Sidebar
$('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
$('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
+
+ $(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: '/' })
+
+ $(document)
+ .off 'click', '.js-issuable-todo'
+ .on 'click', '.js-issuable-todo', @toggleTodo
+
+ toggleTodo: (e) =>
+ $this = $(e.currentTarget)
+ $todoLoading = $('.js-issuable-todo-loading')
+ $btnText = $('.js-issuable-todo-text', $this)
+ ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
+
+ if $this.attr('data-delete-path')
+ url = "#{$this.attr('data-delete-path')}"
+ else
+ url = "#{$this.data('url')}"
+
+ $.ajax(
+ url: url
+ type: ajaxType
+ dataType: 'json'
+ data:
+ issuable_id: $this.data('issuable-id')
+ issuable_type: $this.data('issuable-type')
+ beforeSend: =>
+ @beforeTodoSend($this, $todoLoading)
+ ).done (data) =>
+ @todoUpdateDone(data, $this, $btnText, $todoLoading)
+
+ beforeTodoSend: ($btn, $todoLoading) ->
+ $btn.disable()
+ $todoLoading.removeClass 'hidden'
+
+ todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
+ $todoPendingCount = $('.todos-pending-count')
+ $todoPendingCount.text data.count
+
+ $btn.enable()
+ $todoLoading.addClass 'hidden'
+
+ if data.count is 0
+ $todoPendingCount.addClass 'hidden'
+ else
+ $todoPendingCount.removeClass 'hidden'
+
+ if data.delete_path?
+ $btn
+ .attr 'aria-label', $btn.data('mark-text')
+ .attr 'data-delete-path', data.delete_path
+ $btnText.text $btn.data('mark-text')
+ else
+ $btn
+ .attr 'aria-label', $btn.data('todo-text')
+ .removeAttr 'data-delete-path'
+ $btnText.text $btn.data('todo-text')
+
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
img = $sidebarCollapsedIcon.find('img')
@@ -76,12 +163,10 @@ class @Sidebar
@triggerOpenSidebar() if not @isOpen()
if action is 'hide'
- @triggerOpenSidebar() is @isOpen()
+ @triggerOpenSidebar() if @isOpen()
isOpen: ->
@sidebar.is('.right-sidebar-expanded')
getBlock: (name) ->
@sidebar.find(".block.#{name}")
-
-
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index 6a7b4ad1db7..72b1d3dfb1e 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -20,8 +20,7 @@ class @SearchAutocomplete
@dropdown = @wrap.find('.dropdown')
@dropdownContent = @dropdown.find('.dropdown-content')
- @locationBadgeEl = @getElement('.search-location-badge')
- @locationText = @getElement('.location-text')
+ @locationBadgeEl = @getElement('.location-badge')
@scopeInputEl = @getElement('#scope')
@searchInput = @getElement('.search-input')
@projectInputEl = @getElement('#search_project_id')
@@ -68,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) ->
_this = @
- # Do not trigger request if input is empty
- return if @searchInput.val() is ''
+ unless term
+ if contents = @getCategoryContents()
+ @searchInput.data('glDropdown').filter.options.callback contents
+ @enableAutocomplete()
+
+ return
# Prevent multiple ajax calls
return if @loadingSuggestions
@@ -123,6 +126,37 @@ class @SearchAutocomplete
).always ->
_this.loadingSuggestions = false
+
+ getCategoryContents: ->
+
+ userId = gon.current_user_id
+ { utils, projectOptions, groupOptions, dashboardOptions } = gl
+
+ if utils.isInGroupsPage() and groupOptions
+ options = groupOptions[utils.getGroupSlug()]
+
+ else if utils.isInProjectPage() and projectOptions
+ options = projectOptions[utils.getProjectSlug()]
+
+ else if dashboardOptions
+ options = dashboardOptions
+
+ { issuesPath, mrPath, name } = options
+
+ items = [
+ { header: "#{name}" }
+ { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
+ { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
+ 'separator'
+ { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
+ { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
+ ]
+
+ items.splice 0, 1 unless name
+
+ return items
+
+
serializeState: ->
{
# Search Criteria
@@ -133,33 +167,31 @@ class @SearchAutocomplete
scope: @scopeInputEl.val()
# Location badge
- _location: @locationText.text()
+ _location: @locationBadgeEl.text()
}
bindEvents: ->
- $(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
+ @searchInput.on 'blur', @onSearchInputBlur
@clearInput.on 'click', @onClearInputClick
-
- onDocumentClick: (e) =>
- # If clicking outside the search box
- # And search input is not focused
- # And we are not clicking inside a suggestion
- if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
- @onSearchInputBlur()
+ @locationBadgeEl.on 'click', =>
+ @searchInput.focus()
enableAutocomplete: ->
# No need to enable anything if user is not logged in
return if !gon.current_user_id
- _this = @
- @loadingSuggestions = false
+ unless @dropdown.hasClass('open')
+ _this = @
+ @loadingSuggestions = false
- @dropdown.addClass('open')
- @searchInput.removeClass('disabled')
+ @dropdown
+ .addClass('open')
+ .trigger('shown.bs.dropdown')
+ @searchInput.removeClass('disabled')
onSearchInputKeyDown: =>
# Saves last length of the entered text
@@ -190,7 +222,7 @@ class @SearchAutocomplete
@disableAutocomplete()
else
# We should display the menu only when input is not empty
- @enableAutocomplete()
+ @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value
@@ -205,6 +237,12 @@ class @SearchAutocomplete
@isFocused = true
@wrap.addClass('search-active')
+ @getData() if @getValue() is ''
+
+
+ getValue: -> return @searchInput.val()
+
+
onClearInputClick: (e) =>
e.preventDefault()
@searchInput.val('').focus()
@@ -221,30 +259,29 @@ class @SearchAutocomplete
category = if item.category? then "#{item.category}: " else ''
value = if item.value? then item.value else ''
- html = "<span class='location-badge'>
- <i class='location-text'>#{category}#{value}</i>
- </span>"
- @locationBadgeEl.html(html)
+ badgeText = "#{category}#{value}"
+ @locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
+
+ hasLocationBadge: -> return @wrap.is '.has-location-badge'
+
+
restoreOriginalState: ->
inputs = Object.keys @originalState
for input in inputs
@getElement("##{input}").val(@originalState[input])
-
if @originalState._location is ''
- @locationBadgeEl.empty()
+ @locationBadgeEl.hide()
else
@addLocationBadge(
value: @originalState._location
)
- @dropdown.removeClass 'open'
-
badgePresent: ->
- @locationBadgeEl.children().length
+ @locationBadgeEl.length
resetSearchState: ->
inputs = Object.keys @originalState
@@ -256,13 +293,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
+
removeLocationBadge: ->
- @locationBadgeEl.empty()
- # Reset state
+ @locationBadgeEl.hide()
@resetSearchState()
-
@wrap.removeClass('has-location-badge')
+ @disableAutocomplete()
+
disableAutocomplete: ->
@searchInput.addClass('disabled')
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index f3d66004138..3319a67a79d 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -1,7 +1,7 @@
class @Shortcuts
- constructor: ->
+ constructor: (skipResetBindings) ->
@enabledHelp = []
- Mousetrap.reset()
+ Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
@@ -9,12 +9,12 @@ class @Shortcuts
onToggleHelp: (e) =>
e.preventDefault()
- @toggleHelp(@enabledHelp)
+ Shortcuts.toggleHelp(@enabledHelp)
- toggleMarkdownPreview: (e) =>
+ toggleMarkdownPreview: (e) ->
$(document).triggerHandler('markdown-preview:toggle', [e])
- toggleHelp: (location) ->
+ @toggleHelp: (location) ->
$modal = $('#modal-shortcuts')
if $modal.length
diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee
new file mode 100644
index 00000000000..6d21e5d1150
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.coffee
@@ -0,0 +1,10 @@
+#= require shortcuts
+
+class @ShortcutsBlob extends Shortcuts
+ constructor: (skipResetBindings) ->
+ super skipResetBindings
+ Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
+
+ @copyToClipboard: ->
+ clipboardButton = $('.btn-clipboard')
+ clipboardButton.click() if clipboardButton
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
index 4a05bdccdb3..cca2b8a1fcc 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
@@ -3,10 +3,10 @@
class @ShortcutsDashboardNavigation extends Shortcuts
constructor: ->
super()
- Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity'))
- Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues'))
- Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests'))
- Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects'))
+ Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'))
+ Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'))
+ Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'))
+ Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'))
@findAndFollowLink: (selector) ->
link = $(selector).attr('href')
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
index ccb42ab2168..c93bcf3ceec 100644
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@replyWithSelectedText()
return false
)
- Mousetrap.bind('j', =>
- @prevIssue()
- return false
- )
- Mousetrap.bind('k', =>
- @nextIssue()
- return false
- )
Mousetrap.bind('e', =>
@editIssue()
return false
@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
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()
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ea4ac52da31..68009e58645 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -3,24 +3,35 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
- $('header').toggleClass("header-collapsed header-expanded")
- $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
- $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
+ $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
+
+ if $.cookie('pin_nav') is 'true'
+ $('.navbar-fixed-top').toggleClass('header-pinned-nav')
+ $('.page-with-sidebar').toggleClass('page-sidebar-pinned')
setTimeout ( ->
- niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar();
), 300
+$(document)
+ .off 'click', 'body'
+ .on 'click', 'body', (e) ->
+ unless $.cookie('pin_nav') is 'true'
+ $target = $(e.target)
+ $nav = $target.closest('.sidebar-wrapper')
+ pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
+ $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
+
+ if $nav.length is 0 and pageExpanded and $toggle.length is 0
+ $('.page-with-sidebar')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+
+ $('.navbar-fixed-top')
+ .toggleClass('header-collapsed header-expanded')
+
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (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 f27780dda93..01b28171f72 100644
--- a/app/assets/javascripts/star.js.coffee
+++ b/app/assets/javascripts/star.js.coffee
@@ -9,9 +9,11 @@ class @Star
$this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
+ gl.utils.updateTooltipTitle $this, 'Star project'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
+ gl.utils.updateTooltipTitle $this, 'Unstar project'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 1a430f3aa47..08d494aba9f 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -19,3 +19,8 @@ class @Subscription
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
btn.find('span').text(action)
@subscription_status.find('>div').toggleClass('hidden')
+
+ if btn.attr('data-original-title')
+ btn.tooltip('hide')
+ .attr('data-original-title', action)
+ .tooltip('fixTitle')
diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee
index de8eebcd0b2..83de584f2d9 100644
--- a/app/assets/javascripts/tree.js.coffee
+++ b/app/assets/javascripts/tree.js.coffee
@@ -5,9 +5,15 @@ class @TreeView
# Code browser tree slider
# Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
$(".tree-content-holder .tree-item").on 'click', (e) ->
- if (e.target.nodeName != "A")
- path = $('.tree-item-file-name a', this).attr('href')
- Turbolinks.visit(path)
+ $clickedEl = $(e.target)
+ path = $('.tree-item-file-name a', this).attr('href')
+
+ if not $clickedEl.is('a') and not $clickedEl.is('.str-truncated')
+ if e.metaKey or e.which is 2
+ e.preventDefault()
+ window.open path, '_blank'
+ else
+ Turbolinks.visit path
# Show the "Loading commit data" for only the first element
$('span.log_loading:first').removeClass('hide')
diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee
new file mode 100644
index 00000000000..6deb902c8de
--- /dev/null
+++ b/app/assets/javascripts/u2f/authenticate.js.coffee
@@ -0,0 +1,63 @@
+# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
+#
+# State Flow #1: setup -> in_progress -> authenticated -> POST to server
+# State Flow #2: setup -> in_progress -> error -> setup
+
+class @U2FAuthenticate
+ constructor: (@container, u2fParams) ->
+ @appId = u2fParams.app_id
+ @challenges = u2fParams.challenges
+ @signRequests = u2fParams.sign_requests
+
+ start: () =>
+ if U2FUtil.isU2FSupported()
+ @renderSetup()
+ else
+ @renderNotSupported()
+
+ authenticate: () =>
+ u2f.sign(@appId, @challenges, @signRequests, (response) =>
+ if response.errorCode
+ error = new U2FError(response.errorCode)
+ @renderError(error);
+ else
+ @renderAuthenticated(JSON.stringify(response))
+ , 10)
+
+ #############
+ # Rendering #
+ #############
+
+ templates: {
+ "notSupported": "#js-authenticate-u2f-not-supported",
+ "setup": '#js-authenticate-u2f-setup',
+ "inProgress": '#js-authenticate-u2f-in-progress',
+ "error": '#js-authenticate-u2f-error',
+ "authenticated": '#js-authenticate-u2f-authenticated'
+ }
+
+ renderTemplate: (name, params) =>
+ templateString = $(@templates[name]).html()
+ template = _.template(templateString)
+ @container.html(template(params))
+
+ renderSetup: () =>
+ @renderTemplate('setup')
+ @container.find('#js-login-u2f-device').on('click', @renderInProgress)
+
+ renderInProgress: () =>
+ @renderTemplate('inProgress')
+ @authenticate()
+
+ renderError: (error) =>
+ @renderTemplate('error', {error_message: error.message()})
+ @container.find('#js-u2f-try-again').on('click', @renderSetup)
+
+ renderAuthenticated: (deviceResponse) =>
+ @renderTemplate('authenticated')
+ # Prefer to do this instead of interpolating using Underscore templates
+ # because of JSON escaping issues.
+ @container.find("#js-device-response").val(deviceResponse)
+
+ renderNotSupported: () =>
+ @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee
new file mode 100644
index 00000000000..1a2fc3e757f
--- /dev/null
+++ b/app/assets/javascripts/u2f/error.js.coffee
@@ -0,0 +1,13 @@
+class @U2FError
+ constructor: (@errorCode) ->
+ @httpsDisabled = (window.location.protocol isnt 'https:')
+ console.error("U2F Error Code: #{@errorCode}")
+
+ message: () =>
+ switch
+ when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
+ "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
+ when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
+ "This device has already been registered with us."
+ else
+ "There was a problem communicating with your device."
diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee
new file mode 100644
index 00000000000..74472cfa120
--- /dev/null
+++ b/app/assets/javascripts/u2f/register.js.coffee
@@ -0,0 +1,63 @@
+# Register U2F (universal 2nd factor) devices for users to authenticate with.
+#
+# State Flow #1: setup -> in_progress -> registered -> POST to server
+# State Flow #2: setup -> in_progress -> error -> setup
+
+class @U2FRegister
+ constructor: (@container, u2fParams) ->
+ @appId = u2fParams.app_id
+ @registerRequests = u2fParams.register_requests
+ @signRequests = u2fParams.sign_requests
+
+ start: () =>
+ if U2FUtil.isU2FSupported()
+ @renderSetup()
+ else
+ @renderNotSupported()
+
+ register: () =>
+ u2f.register(@appId, @registerRequests, @signRequests, (response) =>
+ if response.errorCode
+ error = new U2FError(response.errorCode)
+ @renderError(error);
+ else
+ @renderRegistered(JSON.stringify(response))
+ , 10)
+
+ #############
+ # Rendering #
+ #############
+
+ templates: {
+ "notSupported": "#js-register-u2f-not-supported",
+ "setup": '#js-register-u2f-setup',
+ "inProgress": '#js-register-u2f-in-progress',
+ "error": '#js-register-u2f-error',
+ "registered": '#js-register-u2f-registered'
+ }
+
+ renderTemplate: (name, params) =>
+ templateString = $(@templates[name]).html()
+ template = _.template(templateString)
+ @container.html(template(params))
+
+ renderSetup: () =>
+ @renderTemplate('setup')
+ @container.find('#js-setup-u2f-device').on('click', @renderInProgress)
+
+ renderInProgress: () =>
+ @renderTemplate('inProgress')
+ @register()
+
+ renderError: (error) =>
+ @renderTemplate('error', {error_message: error.message()})
+ @container.find('#js-u2f-try-again').on('click', @renderSetup)
+
+ renderRegistered: (deviceResponse) =>
+ @renderTemplate('registered')
+ # Prefer to do this instead of interpolating using Underscore templates
+ # because of JSON escaping issues.
+ @container.find("#js-device-response").val(deviceResponse)
+
+ renderNotSupported: () =>
+ @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb
new file mode 100644
index 00000000000..d59341c38b9
--- /dev/null
+++ b/app/assets/javascripts/u2f/util.js.coffee.erb
@@ -0,0 +1,15 @@
+# Helper class for U2F (universal 2nd factor) device registration and authentication.
+
+class @U2FUtil
+ @isU2FSupported: ->
+ if @testMode
+ true
+ else
+ gon.u2f.browser_supports_u2f
+
+ @enableTestMode: ->
+ @testMode = true
+
+<% if Rails.env.test? %>
+U2FUtil.enableTestMode();
+<% end %>
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
index c2aeffe2381..29dad21faed 100644
--- a/app/assets/javascripts/user_tabs.js.coffee
+++ b/app/assets/javascripts/user_tabs.js.coffee
@@ -26,6 +26,10 @@
# Personal projects
# </a>
# </li>
+# <li class="snippets-tab">
+# <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+# </a>
+# </li>
# </ul>
#
# <div class="tab-content">
@@ -41,6 +45,9 @@
# <div class="tab-pane" id="projects">
# Projects content
# </div>
+# <div class="tab-pane" id="snippets">
+# Snippets content
+# </div>
# </div>
#
# <div class="loading-status">
@@ -100,7 +107,7 @@ class @UserTabs
if action is 'activity'
@loadActivities(source)
- if action in ['groups', 'contributed', 'projects']
+ if action in ['groups', 'contributed', 'projects', 'snippets']
@loadTab(source, action)
loadTab: (source, action) ->
@@ -115,6 +122,9 @@ class @UserTabs
@parentEl.find(tabSelector).html(data.html)
@loaded[action] = true
+ # Fix tooltips
+ gl.utils.localTimeAgo($('.js-timeago', tabSelector))
+
loadActivities: (source) ->
return if @loaded['activity'] is true
diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee
new file mode 100644
index 00000000000..91cacfece46
--- /dev/null
+++ b/app/assets/javascripts/users/application.js.coffee
@@ -0,0 +1,2 @@
+#
+#= require_tree .
diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee
new file mode 100644
index 00000000000..c081f023b04
--- /dev/null
+++ b/app/assets/javascripts/users/calendar.js.coffee
@@ -0,0 +1,193 @@
+class @Calendar
+ constructor: (timestamps, @calendar_activities_path) ->
+ @currentSelectedDate = ''
+ @daySpace = 1
+ @daySize = 15
+ @daySizeWithSpace = @daySize + (@daySpace * 2)
+ @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ @months = []
+
+ # Loop through the timestamps to create a group of objects
+ # The group of objects will be grouped based on the day of the week they are
+ @timestampsTmp = []
+ i = 0
+ group = 0
+ _.each timestamps, (count, date) =>
+ newDate = new Date parseInt(date) * 1000
+ day = newDate.getDay()
+
+ # Create a new group array if this is the first day of the week
+ # or if is first object
+ if (day is 0 and i isnt 0) or i is 0
+ @timestampsTmp.push []
+ group++
+
+ innerArray = @timestampsTmp[group-1]
+
+ # Push to the inner array the values that will be used to render map
+ innerArray.push
+ count: count
+ date: newDate
+ day: day
+
+ i++
+
+ # Init color functions
+ @colorKey = @initColorKey()
+ @color = @initColor()
+
+ # Init the svg element
+ @renderSvg(group)
+ @renderDays()
+ @renderMonths()
+ @renderDayTitles()
+ @renderKey()
+
+ @initTooltips()
+
+ renderSvg: (group) ->
+ @svg = d3.select '.js-contrib-calendar'
+ .append 'svg'
+ .attr 'width', (group + 1) * @daySizeWithSpace
+ .attr 'height', 167
+ .attr 'class', 'contrib-calendar'
+
+ renderDays: ->
+ @svg.selectAll 'g'
+ .data @timestampsTmp
+ .enter()
+ .append 'g'
+ .attr 'transform', (group, i) =>
+ _.each group, (stamp, a) =>
+ if a is 0 and stamp.day is 0
+ month = stamp.date.getMonth()
+ x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace
+ lastMonth = _.last(@months)
+ if lastMonth?
+ lastMonthX = lastMonth.x
+
+ if !lastMonth?
+ @months.push
+ month: month
+ x: x
+ else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX
+ @months.push
+ month: month
+ x: x
+
+ "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)"
+ .selectAll 'rect'
+ .data (stamp) ->
+ stamp
+ .enter()
+ .append 'rect'
+ .attr 'x', '0'
+ .attr 'y', (stamp, i) =>
+ (@daySizeWithSpace * stamp.day)
+ .attr 'width', @daySize
+ .attr 'height', @daySize
+ .attr 'title', (stamp) =>
+ contribText = 'No contributions'
+
+ if stamp.count > 0
+ contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}"
+
+ date = dateFormat(stamp.date, 'mmm d, yyyy')
+
+ "#{contribText}<br />#{date}"
+ .attr 'class', 'user-contrib-cell js-tooltip'
+ .attr 'fill', (stamp) =>
+ if stamp.count isnt 0
+ @color(Math.min(stamp.count, 40))
+ else
+ '#ededed'
+ .attr 'data-container', 'body'
+ .on 'click', @clickDay
+
+ renderDayTitles: ->
+ days = [{
+ text: 'M'
+ y: 29 + (@daySizeWithSpace * 1)
+ }, {
+ text: 'W'
+ y: 29 + (@daySizeWithSpace * 3)
+ }, {
+ text: 'F'
+ y: 29 + (@daySizeWithSpace * 5)
+ }]
+ @svg.append 'g'
+ .selectAll 'text'
+ .data days
+ .enter()
+ .append 'text'
+ .attr 'text-anchor', 'middle'
+ .attr 'x', 8
+ .attr 'y', (day) ->
+ day.y
+ .text (day) ->
+ day.text
+ .attr 'class', 'user-contrib-text'
+
+ renderMonths: ->
+ @svg.append 'g'
+ .selectAll 'text'
+ .data @months
+ .enter()
+ .append 'text'
+ .attr 'x', (date) ->
+ date.x
+ .attr 'y', 10
+ .attr 'class', 'user-contrib-text'
+ .text (date) =>
+ @monthNames[date.month]
+
+ renderKey: ->
+ keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
+ @svg.append 'g'
+ .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})"
+ .selectAll 'rect'
+ .data keyColors
+ .enter()
+ .append 'rect'
+ .attr 'width', @daySize
+ .attr 'height', @daySize
+ .attr 'x', (color, i) =>
+ @daySizeWithSpace * i
+ .attr 'y', 0
+ .attr 'fill', (color) ->
+ color
+
+ initColor: ->
+ colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
+ d3.scale
+ .threshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange)
+
+ initColorKey: ->
+ d3.scale
+ .linear()
+ .range(['#acd5f2', '#254e77'])
+ .domain([0, 3])
+
+ clickDay: (stamp) =>
+ if @currentSelectedDate isnt stamp.date
+ @currentSelectedDate = stamp.date
+ formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate()
+
+ $.ajax
+ url: @calendar_activities_path
+ data:
+ date: formatted_date
+ cache: false
+ dataType: 'html'
+ beforeSend: ->
+ $('.user-calendar-activities').html '<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'
+ success: (data) ->
+ $('.user-calendar-activities').html data
+ else
+ $('.user-calendar-activities').html ''
+
+ initTooltips: ->
+ $('.js-contrib-calendar .js-tooltip').tooltip
+ html: true
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index b80b1b861cc..4e032ab1ff1 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -31,7 +31,7 @@ class @UsersSelect
assignTo = (selected) ->
data = {}
data[abilityName] = {}
- data[abilityName].assignee_id = selected
+ data[abilityName].assignee_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
@@ -61,8 +61,8 @@ class @UsersSelect
collapsedAssigneeTemplate = _.template(
'<% if( avatar ) { %>
- <a class="author_link" href="/u/<%= username %>">
- <img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>">
+ <a class="author_link" href="/u/<%- username %>">
+ <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>">
<span class="author">Toni Boehm</span>
</a>
<% } else { %>
@@ -72,17 +72,17 @@ class @UsersSelect
assigneeTemplate = _.template(
'<% if (username) { %>
- <a class="author_link " href="/u/<%= username %>">
+ <a class="author_link bold" href="/u/<%- username %>">
<% if( avatar ) { %>
- <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
+ <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>">
<% } %>
- <span class="author"><%= name %></span>
+ <span class="author"><%- name %></span>
<span class="username">
- @<%= username %>
+ @<%- username %>
</span>
</a>
<% } else { %>
- <span class="assign-yourself">
+ <span class="no-value assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
@@ -93,6 +93,8 @@ class @UsersSelect
$dropdown.glDropdown(
data: (term, callback) =>
+ isAuthorFilter = $('.js-author-search')
+
@users term, (users) =>
if term.length is 0
showDivider = 0
@@ -138,7 +140,7 @@ class @UsersSelect
toggleLabel: (selected) ->
if selected && 'id' of selected
- selected.name
+ if selected.text then selected.text else selected.name
else
defaultLabel
@@ -147,7 +149,7 @@ class @UsersSelect
hidden: (e) ->
$selectbox.hide()
# display:block overrides the hide-collapse rule
- $value.removeAttr('style')
+ $value.css('display', '')
clicked: (user) ->
page = $('body').data 'page'
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 69b3b6586de..8b93665d085 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -8,9 +8,7 @@
*= require select2
*= require_self
*= require dropzone/basic
- *= require cal-heatmap
*= require cropper.css
- *= require animate
*/
/*
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 560de9fc0bd..a306b8f3f29 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -5,6 +5,7 @@
@import 'framework/tw_bootstrap';
@import "framework/layout";
+@import "framework/animations.scss";
@import "framework/avatar.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
@@ -36,3 +37,4 @@
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
+@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
new file mode 100644
index 00000000000..1fec61bdba1
--- /dev/null
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -0,0 +1,72 @@
+// This file is based off animate.css 3.5.1, available here:
+// https://github.com/daneden/animate.css/blob/3.5.1/animate.css
+//
+// animate.css - http://daneden.me/animate
+// Version - 3.5.1
+// Licensed under the MIT license - http://opensource.org/licenses/MIT
+//
+// Copyright (c) 2016 Daniel Eden
+
+.animated {
+ -webkit-animation-duration: 1s;
+ animation-duration: 1s;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+}
+
+.animated.infinite {
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+}
+
+.animated.hinge {
+ -webkit-animation-duration: 2s;
+ animation-duration: 2s;
+}
+
+.animated.flipOutX,
+.animated.flipOutY,
+.animated.bounceIn,
+.animated.bounceOut {
+ -webkit-animation-duration: .75s;
+ animation-duration: .75s;
+}
+
+@-webkit-keyframes pulse {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes pulse {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.pulse {
+ -webkit-animation-name: pulse;
+ animation-name: pulse;
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index f5ce70b606b..bb8d71fbae8 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -45,6 +45,7 @@
&.s32 { font-size: 20px; line-height: 32px; }
&.s40 { font-size: 16px; line-height: 40px; }
&.s60 { font-size: 32px; line-height: 60px; }
+ &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 90px; }
&.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 140px; }
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
new file mode 100644
index 00000000000..d28cda6d62d
--- /dev/null
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -0,0 +1,44 @@
+.blank-state-welcome {
+ text-align: center;
+ border-bottom: 1px solid $border-color;
+
+ .blank-state-text {
+ margin-bottom: 0;
+ }
+}
+
+.blank-state {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
+.blank-state-no-icon {
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+
+.blank-state-icon {
+ padding-bottom: 20px;
+
+ path {
+ fill: $gray-darkest;
+ }
+}
+
+.blank-state-title {
+ margin-top: 0;
+ margin-bottom: 5px;
+ font-size: 19px;
+ font-weight: normal;
+}
+
+.blank-state-text {
+ margin-top: 0;
+ margin-bottom: $gl-padding;
+ font-size: 15px;
+}
+
+.blank-state-welcome-title {
+ font-size: 24px;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 434a26d57c6..41e77a4ac68 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -24,8 +24,8 @@
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0;
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
color: $gl-gray;
&.oneline-block {
@@ -61,6 +61,11 @@
margin-bottom: -$gl-padding;
}
+ &.content-component-block {
+ padding: 11px 0;
+ background-color: $white-light;
+ }
+
.title {
color: $gl-text-color;
}
@@ -86,6 +91,26 @@
background-color: $white-light;
border-top: none;
}
+
+ &.top-block .container-fluid {
+ background-color: inherit;
+ }
+}
+
+.sub-header-block {
+ background-color: $white-light;
+ border-bottom: 1px solid $white-dark;
+ padding: 11px 0;
+ margin-bottom: 11px;
+
+ .oneline {
+ line-height: 35px;
+ }
+
+ &.no-bottom-space {
+ border-bottom: 0;
+ margin-bottom: 0;
+ }
}
.cover-block {
@@ -110,9 +135,9 @@
.cover-title {
color: $gl-header-color;
margin: 0;
- font-size: 23px;
+ font-size: 24px;
font-weight: normal;
- margin: 16px 0 5px;
+ margin-bottom: 10px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
@@ -137,7 +162,6 @@
}
.cover-desc {
- padding: 0 $gl-padding 3px;
color: $gl-text-color;
&.username:last-child {
@@ -205,7 +229,7 @@
.content-block {
padding: $gl-padding 0;
- border-bottom: 1px solid $border-color;
+ border-bottom: 1px solid $white-dark;
&.oneline-block {
line-height: 36px;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index eaf85bb17ca..590b8f54363 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -16,6 +16,19 @@
@include btn-default;
}
+@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) {
+ background-color: $background;
+ color: $text;
+ border-color: $border;
+
+ &:hover,
+ &:focus {
+ background-color: $hover-background;
+ color: $hover-text;
+ border-color: $hover-border;;
+ }
+}
+
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light;
border-color: $border-light;
@@ -66,6 +79,23 @@
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
}
+@mixin btn-with-margin {
+ margin-left: $btn-side-margin;
+ float: left;
+
+ &.inline {
+ float: none;
+ }
+
+ &.btn-sm {
+ margin-left: $btn-sm-side-margin;
+ }
+
+ &.btn-xs {
+ margin-left: $btn-xs-side-margin;
+ }
+}
+
.btn {
@include btn-default;
@include btn-white;
@@ -106,11 +136,14 @@
@include btn-blue;
}
- &.btn-close,
&.btn-warning {
@include btn-orange;
}
+ &.btn-close {
+ @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
+ }
+
&.btn-danger,
&.btn-remove,
&.btn-red {
@@ -126,15 +159,9 @@
}
&.btn-grouped {
- margin-right: 7px;
- float: left;
- &:last-child {
- margin-right: 0;
- }
- &.btn-xs {
- margin-right: 3px;
- }
+ @include btn-with-margin;
}
+
&.disabled {
pointer-events: auto !important;
}
@@ -176,11 +203,7 @@
.btn-group {
&.btn-grouped {
- margin-right: 7px;
- float: left;
- &:last-child {
- margin-right: 0;
- }
+ @include btn-with-margin;
}
}
@@ -258,3 +281,21 @@
color: $gl-icon-color;
}
}
+
+.clone-dropdown-btn a {
+ color: $dropdown-link-color;
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.btn-static {
+ background-color: $background-color !important;
+ border: 1px solid lightgrey;
+ cursor: default;
+ &:active {
+ -moz-box-shadow: inset 0 0 0 white;
+ -webkit-box-shadow: inset 0 0 0 white;
+ box-shadow: inset 0 0 0 white;
+ }
+}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 11f39d583bd..8642b7530e2 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,70 +1,44 @@
.calender-block {
+ padding-left: 0;
+ padding-right: 0;
+
@media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
overflow-x: scroll;
}
}
.user-calendar-activities {
- .calendar_onclick_hr {
- padding: 0;
- margin: 10px 0;
- }
-
.str-truncated {
max-width: 70%;
}
- .text-expander {
- background: #eee;
- color: #555;
- padding: 0 5px;
- cursor: pointer;
- margin-left: 4px;
- &:hover {
- background-color: #ddd;
- }
+ .user-calendar-activities-loading {
+ font-size: 24px;
}
}
-/**
-* This overwrites the default values of the cal-heatmap gem
-*/
-.calendar {
- .qi {
- fill: #fff;
- }
-
- .q1 {
- fill: #ededed !important;
- }
+.user-calendar {
+ text-align: center;
- .q2 {
- fill: #acd5f2 !important;
- }
-
- .q3 {
- fill: #7fa8d1 !important;
- }
-
- .q4 {
- fill: #49729b !important;
- }
-
- .q5 {
- fill: #254e77 !important;
+ .calendar {
+ display: inline-block;
}
+}
- .future {
- visibility: hidden;
+.user-contrib-cell {
+ &:hover {
+ cursor: pointer;
+ stroke: #000;
}
+}
- .domain-background {
- fill: none;
- shape-rendering: crispedges;
- }
+.user-contrib-text {
+ font-size: 12px;
+ fill: #959494;
+}
- .ch-tooltip {
- padding: 3px;
- font-weight: 550;
- }
+.calendar-hint {
+ margin-top: -23px;
+ float: right;
+ font-size: 12px;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 3386523dbf7..f8aecd0558d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -289,7 +289,7 @@ table {
text-shadow: none;
@media (min-width: $screen-sm-min) {
- margin-top: 11px;
+ margin-top: 8px;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 4bf3a050403..d4e900f80ef 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -20,8 +20,12 @@
}
.open {
- .dropdown-menu {
+ .dropdown-menu,
+ .dropdown-menu-nav {
display: block;
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
}
.dropdown-menu-toggle {
@@ -64,9 +68,14 @@
color: $dropdown-toggle-hover-icon-color;
}
}
+
+ &.large {
+ width: 200px;
+ }
}
-.dropdown-menu {
+.dropdown-menu,
+.dropdown-menu-nav {
display: none;
position: absolute;
top: 100%;
@@ -77,7 +86,7 @@
margin-bottom: 0;
font-size: 15px;
font-weight: normal;
- padding: 10px 0;
+ padding: 8px 0;
background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
@@ -101,12 +110,12 @@
li {
text-align: left;
list-style: none;
- padding: 0 10px;
+ padding: 0 8px;
}
.divider {
height: 1px;
- margin: 8px 10px;
+ margin: 8px;
padding: 0;
background-color: $dropdown-divider-color;
}
@@ -122,10 +131,9 @@
a {
display: block;
position: relative;
- padding-left: 10px;
- padding-right: 10px;
+ padding: 5px 8px;
color: $dropdown-link-color;
- line-height: 34px;
+ line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
@@ -154,7 +162,7 @@
color: $dropdown-header-color;
font-size: 13px;
line-height: 22px;
- padding: 0 10px 10px;
+ padding: 0 10px;
}
.separator + .dropdown-header {
@@ -162,6 +170,20 @@
}
}
+.dropdown-menu-large {
+ width: 340px;
+}
+
+.dropdown-menu-no-wrap {
+ a {
+ white-space: normal;
+ }
+}
+
+.dropdown-menu-full-width {
+ width: 100%;
+}
+
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
@@ -228,13 +250,11 @@
a {
padding-left: 25px;
- &.is-active {
+ &.is-indeterminate, &.is-active {
&::before {
- content: "\f00c";
position: absolute;
left: 5px;
- top: 50%;
- margin-top: -7px;
+ top: 8px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
@@ -242,6 +262,14 @@
-moz-osx-font-smoothing: grayscale;
}
}
+
+ &.is-indeterminate::before {
+ content: "\f068";
+ }
+
+ &.is-active::before {
+ content: "\f00c";
+ }
}
}
@@ -442,10 +470,12 @@
}
}
- .ui-state-active,
- .ui-state-hover {
- color: $md-link-color;
- background-color: $calendar-hover-bg;
+ .ui-datepicker-calendar {
+ .ui-state-hover,
+ .ui-state-active {
+ color: #fff;
+ border: 0;
+ }
}
.ui-datepicker-prev,
@@ -521,3 +551,14 @@
background-color: $calendar-unselectable-bg;
}
}
+
+.dropdown-menu-inner-title {
+ display: block;
+ color: $gl-title-color;
+ font-weight: 600;
+}
+
+.dropdown-menu-inner-content {
+ display: block;
+ color: $gl-placeholder-color;
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 61d9954c6c8..71e4b50f2af 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -5,6 +5,10 @@
.file-holder {
border: 1px solid $border-color;
+ &.file-holder-no-border {
+ border: 0;
+ }
+
&.readme-holder {
margin: $gl-padding-top 0;
}
@@ -23,8 +27,17 @@
word-wrap: break-word;
border-radius: 3px 3px 0 0;
+ &.file-title-clear {
+ padding-left: 0;
+ padding-right: 0;
+ background-color: transparent;
+
+ .file-actions {
+ right: 0;
+ }
+ }
+
.file-actions {
- float: right;
position: absolute;
top: 5px;
right: 15px;
@@ -36,24 +49,8 @@
}
}
- .filename {
- &.old {
- display: inline-block;
- span.idiff {
- background-color: #f8cbcb;
- }
- }
-
- &.new {
- display: inline-block;
- span.idiff {
- background-color: #a6f3a6;
- }
- }
- }
-
a:not(.btn) {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.left-options {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 1bfd0213995..a951a2b97fe 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -16,4 +16,11 @@
@extend .alert-danger;
margin: 0;
}
+
+ .flash-notice, .flash-alert {
+ .container-fluid.flash-text {
+ background: transparent;
+ }
+ }
}
+
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 558b133f593..43d55661541 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -28,10 +28,6 @@ input[type='text'].danger {
}
label {
- &.control-label {
- @extend .col-sm-2;
- }
-
&.inline-label {
margin: 0;
}
@@ -41,6 +37,10 @@ label {
}
}
+.control-label {
+ @extend .col-sm-2;
+}
+
.inline-input-group {
width: 250px;
}
@@ -76,6 +76,7 @@ label {
.form-control {
@include box-shadow(none);
border-radius: 3px;
+ padding: $gl-vert-padding $gl-input-padding;
}
.select-wrapper {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index dc4668877f2..d1ff63bd099 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -8,48 +8,18 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
- .header-logo {
- background-color: $color;
- border-color: $color;
-
- a {
- color: $color-light;
-
- h3 {
- color: $color-light;
- }
- }
+ .toggle-nav-collapse,
+ .pin-nav-btn {
+ color: $color-light;
+ background: $color;
&:hover {
- background-color: $color-darker;
- a {
- color: #fff;
-
- h3 {
- color: #fff;
- }
- }
+ color: $white-light;
}
}
- .collapse-nav a {
- color: #fff;
- background: $color;
- }
-
.sidebar-wrapper {
background: $color-darker;
-
- .sidebar-user {
- background: $color-darker;
- color: $color-light;
-
- &:hover {
- background-color: $color-dark;
- color: #fff;
- text-decoration: none;
- }
- }
}
.nav-sidebar li {
@@ -64,10 +34,20 @@
color: $color-light;
}
+ path,
+ polygon {
+ fill: $color-light;
+ }
+
.count {
color: $color-light;
background: $color-dark;
}
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
}
&.separate-item {
@@ -75,7 +55,7 @@
}
&.active a {
- color: #fff;
+ color: $white-light;
background: $color-dark;
&.no-highlight {
@@ -83,16 +63,24 @@
}
i {
- color: #fff
+ color: $white-light
+ }
+
+ path,
+ polygon {
+ fill: $white-light;
}
}
}
}
}
+$theme-charcoal: #3d454d;
+$theme-charcoal-dark: #383f45;
+$theme-charcoal-text: #b9bbbe;
+
$theme-blue: #2980b9;
-$theme-charcoal: #333c47;
-$theme-graphite: #888;
+$theme-graphite: #666;
$theme-gray: #373737;
$theme-green: #019875;
$theme-violet: #548;
@@ -103,11 +91,11 @@ body {
}
&.ui_charcoal {
- @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d);
+ @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
}
&.ui_graphite {
- @include gitlab-theme(#ccc, $theme-graphite, #777, #666);
+ @include gitlab-theme(#ccc, #777, $theme-graphite, #555);
}
&.ui_gray {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 8190a97ed58..0c607071840 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,20 +2,30 @@
* Application Header
*
*/
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover,
+ &.highlight {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
header {
- transition-duration: .3s;
+ transition: padding $sidebar-transition-duration;
&.navbar-empty {
- height: 58px;
+ height: $header-height;
background: #fff;
border-bottom: 1px solid $btn-gray-hover;
.center-logo {
- margin: 11px 0;
+ margin: 8px 0;
text-align: center;
#tanuki-logo, img {
- width: 36px;
height: 36px;
}
}
@@ -50,7 +60,7 @@ header {
margin: ($header-height - 28) / 2 0;
margin-left: 10px;
height: 28px;
- width: 28px;
+ min-width: 28px;
line-height: 28px;
text-align: center;
@@ -82,10 +92,10 @@ header {
}
.side-nav-toggle {
- display: none;
position: absolute;
left: -10px;
margin: 6px 0;
+ font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $background-color;
@@ -97,21 +107,13 @@ header {
&:focus {
outline: none;
}
-
- @media (max-width: $screen-xs-min) {
- display: block;
- }
}
}
.header-content {
position: relative;
height: $header-height;
- padding-right: 40px;
-
- @media (max-width: $screen-xs-min) {
- padding-left: 40px;
- }
+ padding-left: 30px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
@@ -121,9 +123,33 @@ header {
margin-top: -5px;
}
+ .header-logo {
+ position: absolute;
+ left: 50%;
+ margin-left: -18px;
+ top: 7px;
+ transition-duration: .3s;
+ z-index: 999;
+
+ svg, img {
+ height: 36px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ right: 25px;
+ left: auto;
+ }
+ }
+
.title {
margin: 0;
font-size: 19px;
+ max-width: 400px;
+ display: inline-block;
line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
@@ -132,6 +158,10 @@ header {
vertical-align: top;
white-space: nowrap;
+ @media (max-width: $screen-sm-max) {
+ max-width: 190px;
+ }
+
a {
color: $gl-text-color;
&:hover {
@@ -159,6 +189,10 @@ header {
.navbar-collapse {
float: right;
border-top: none;
+
+ @media (max-width: $screen-xs-max) {
+ float: none;
+ }
}
}
@@ -171,32 +205,24 @@ header {
}
}
-@mixin collapsed-header {
- margin-left: $sidebar_collapsed_width;
-}
-
-.header-collapsed {
- margin-left: $sidebar_collapsed_width;
+#tanuki-logo {
- @media (min-width: $screen-md-min) {
- @include collapsed-header;
+ #tanuki-left-ear,
+ #tanuki-right-ear,
+ #tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
}
- @media (max-width: $screen-xs-min) {
- margin-left: 0;
+ #tanuki-left-eye,
+ #tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
}
-}
-.header-expanded {
- margin-left: $sidebar_collapsed_width;
-
- @media (min-width: $screen-md-min) {
- margin-left: $sidebar_width;
+ #tanuki-left-cheek,
+ #tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
}
- @media (max-width: $screen-xs-min) {
- margin-left: 0;
- }
}
@media (max-width: $screen-xs-max) {
@@ -215,14 +241,23 @@ header {
.navbar-collapse {
padding-left: 5px;
- li {
+ .nav > li {
display: table-cell;
width: 1%;
-
- a {
- margin-left: 8px !important;
- }
}
}
}
}
+
+.header-user {
+ .dropdown-menu-nav {
+ width: 140px;
+ margin-top: -5px;
+ }
+}
+
+.header-user-avatar {
+ float: left;
+ margin-right: 5px;
+ border-radius: 50%;
+}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 525ed81b059..30a5b837d69 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,6 +2,7 @@
font-family: $regular_font;
font-size: $font-size-base;
+ &.ui-datepicker,
&.ui-datepicker-inline {
border: 1px solid #ddd;
padding: 10px;
@@ -10,6 +11,25 @@
.ui-datepicker-header {
background: #fff;
border-color: #ddd;
+
+ .ui-datepicker-prev,
+ .ui-datepicker-next {
+ top: 4px;
+ }
+
+ .ui-datepicker-prev {
+ left: 2px;
+ }
+
+ .ui-datepicker-next {
+ right: 2px;
+ }
+
+ .ui-state-hover {
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ }
}
.ui-datepicker-calendar td a {
@@ -36,21 +56,18 @@
}
.ui-state-highlight {
- border: 1px solid #eee;
- background: #eee;
+ border: 0;
+ background: transparent;
}
- .ui-state-active {
- border: 1px solid $gl-primary;
- background: $gl-primary;
- color: #fff;
- }
-
- .ui-state-hover,
- .ui-state-focus {
- border: 1px solid $row-hover;
- background: $row-hover;
- color: #333;
+ .ui-datepicker-calendar {
+ .ui-state-active,
+ .ui-state-hover,
+ .ui-state-focus {
+ border: 1px solid $gl-primary;
+ background: $gl-primary;
+ color: #fff;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index b17c8bcbb1e..aed0b44d91b 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -137,9 +137,38 @@ ul.content-list {
padding-top: 1px;
float: right;
- .btn {
- padding: 10px 14px;
+ > .control-text {
+ margin-right: $gl-padding-top;
+ line-height: 40px;
+
+ &:last-child {
+ margin-right: 0;
+ }
}
+
+ > .btn,
+ > .btn-group {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+ margin-top: 4px;
+ margin-bottom: 4px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ // When dragging a list item
+ &.ui-sortable-helper {
+ border-bottom: none;
+ }
+
+ &.list-placeholder {
+ background-color: $gray-light;
+ border: dotted 1px $gray-dark;
+ margin: 1px 0;
+ min-height: 52px;
}
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index fd885b38680..5d3273ea64d 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,6 +65,11 @@
a {
padding-top: 0;
line-height: 1;
+ border-bottom: 1px solid $border-color;
+
+ &.btn.btn-xs {
+ padding: 2px 5px;
+ }
}
}
}
@@ -97,5 +102,31 @@
white-space: pre-wrap;
word-break: keep-all;
}
+
+ @include bulleted-list;
+ }
+}
+
+.toolbar-group {
+ float: left;
+ margin-right: -5px;
+ margin-left: $gl-padding;
+
+ &:first-child {
+ margin-left: 0;
+ }
+}
+
+.toolbar-btn {
+ float: left;
+ padding: 0 5px;
+ color: #959494;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover,
+ &:focus {
+ color: $gl-link-color;
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 250d6309291..5ec5a96a597 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -2,18 +2,10 @@
* Generic mixins
*/
@mixin box-shadow($shadow) {
- -webkit-box-shadow: $shadow;
- -moz-box-shadow: $shadow;
- -ms-box-shadow: $shadow;
- -o-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin border-radius($radius) {
- -webkit-border-radius: $radius;
- -moz-border-radius: $radius;
- -ms-border-radius: $radius;
- -o-border-radius: $radius;
border-radius: $radius;
}
@@ -118,3 +110,17 @@
font-size: 16px;
line-height: 24px;
}
+
+@mixin bulleted-list {
+ > ul {
+ list-style-type: disc;
+
+ ul {
+ list-style-type: circle;
+
+ ul {
+ list-style-type: square;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 33cbee85987..367c7d01944 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -48,20 +48,33 @@
display: block;
}
- .project-home-desc {
- font-size: 21px;
- }
-
.project-repo-buttons,
.git-clone-holder {
display: none;
}
+
+ // Display Star and Fork buttons without counters on mobile.
+ .project-action-buttons {
+ display: block;
+
+ .count-buttons .btn {
+ margin: 0 10px;
+ }
+
+ .count-buttons .count-with-arrow {
+ display: none;
+ }
+ }
}
.project-stats {
display: none;
}
+ .group-right-buttons {
+ display: none;
+ }
+
.container .title {
padding-left: 15px !important;
}
@@ -70,10 +83,6 @@
display: none;
}
- %ul.notes .note-role, .note-actions {
- display: none;
- }
-
.nav-links, .nav-links {
li a {
font-size: 14px;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index f2d7dc625e0..02ea98e9d94 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,3 +1,42 @@
+@mixin fade($gradient-direction, $rgba, $gradient-color) {
+ visibility: hidden;
+ opacity: 0;
+ z-index: 2;
+ position: absolute;
+ bottom: 12px;
+ width: 43px;
+ height: 30px;
+ transition-duration: .3s;
+ -webkit-transform: translateZ(0);
+ background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+
+ &.scrolling {
+ visibility: visible;
+ opacity: 1;
+ transition-duration: .3s;
+ }
+
+ .fa {
+ position: relative;
+ top: 5px;
+ font-size: 18px;
+ }
+}
+
+@mixin scrolling-links() {
+ white-space: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
.nav-links {
padding: 0;
margin: 0;
@@ -10,8 +49,7 @@
a {
display: inline-block;
- padding: 14px;
- padding-top: $gl-padding;
+ padding: $gl-btn-padding;
padding-bottom: 11px;
margin-bottom: -1px;
font-size: 15px;
@@ -36,6 +74,29 @@
color: #78a;
}
}
+
+ &.sub-nav {
+ text-align: center;
+ background-color: $background-color;
+
+ .container-fluid {
+ background-color: $background-color;
+ margin-bottom: 0;
+ }
+
+ li {
+
+ a {
+ margin: 0;
+ padding: 11px 10px 9px;
+ }
+
+ &.active a {
+ border-bottom: none;
+ color: $link-underline-blue;
+ }
+ }
+ }
}
.top-area {
@@ -73,8 +134,17 @@
margin-bottom: 0;
border-bottom: none;
+ &.wide {
+ width: 100%;
+ display: block;
+ }
+
+ li a {
+ padding: 16px 10px 11px;
+ }
+
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-max) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
@@ -99,6 +169,7 @@
> .btn {
margin-right: $gl-padding-top;
display: inline-block;
+ vertical-align: top;
&:last-child {
margin-right: 0;
@@ -119,7 +190,7 @@
}
input {
- height: 34px;
+ height: 35px;
display: inline-block;
position: relative;
top: 2px;
@@ -148,7 +219,7 @@
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
-
+ width: 100%;
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px;
display: block;
@@ -158,6 +229,7 @@
form {
display: block;
height: auto;
+ margin-bottom: 14px;
input {
width: 100%;
@@ -179,15 +251,11 @@
margin: 0;
}
}
+ }
- /* Small devices (tablets, 768px and lower) */
- @media (max-width: $screen-sm-max) {
- width: 100%;
- text-align: left;
-
- input {
- width: 300px;
- }
+ &.adjust {
+ .nav-text, .nav-controls {
+ width: auto;
}
}
}
@@ -196,10 +264,11 @@
position: fixed;
top: $header-height;
width: 100%;
- z-index: 1;
+ z-index: 11;
background: $background-color;
border-bottom: 1px solid $border-color;
- transition-duration: .3s;
+ transition: padding $sidebar-transition-duration;
+ text-align: center;
.container-fluid {
position: relative;
@@ -209,6 +278,10 @@
float: right;
padding: 7px 0 0;
+ @media (max-width: $screen-sm-max) {
+ display: none;
+ }
+
i {
color: $layout-link-gray;
}
@@ -224,15 +297,20 @@
}
.dropdown {
- margin-left: 7px;
+ position: absolute;
+ top: 7px;
+ right: 15px;
+ z-index: 2;
+
+ li.active {
+ font-weight: bold;
+ }
}
}
.nav-links {
border-bottom: none;
height: 51px;
- white-space: nowrap;
- overflow-x: auto;
li {
@@ -245,19 +323,118 @@
}
&.active {
+
a, i {
color: $black;
}
+
+ svg {
+ path,
+ polygon {
+ fill: $black;
+ }
+ }
}
.badge {
color: $gl-icon-color;
}
+
+ &:hover {
+ a, i {
+ color: $black;
+ }
+ }
+ }
+ }
+}
+
+.scrolling-tabs-container {
+ position: relative;
+
+ .nav-links {
+ @include scrolling-links();
+ }
+
+ .fade-right {
+ @include fade(left, rgba(255, 255, 255, 0.4), $background-color);
+ right: -5px;
+
+ .fa {
+ right: -7px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, rgba(255, 255, 255, 0.4), $background-color);
+ left: -5px;
+
+ .fa {
+ left: -7px;
+ }
+ }
+
+ &.sub-nav-scroll {
+
+ .fade-right {
+ right: 0;
+
+ .fa {
+ right: -23px;
+ }
+ }
+
+ .fade-left {
+ left: 0;
+
+ .fa {
+ left: 10px;
+ }
}
}
+}
+
+.nav-block {
+ position: relative;
+
+ .nav-links {
+ @include scrolling-links();
+
+ .fade-right {
+ @include fade(left, rgba(255, 255, 255, 0.4), $white-light);
+ right: -5px;
+
+ .fa {
+ right: -7px;
+ }
+ }
+ .fade-left {
+ @include fade(right, rgba(255, 255, 255, 0.4), $white-light);
+ left: -5px;
+
+ .fa {
+ left: -7px;
+ }
+ }
+ }
}
.page-with-layout-nav {
- margin-top: 50px;
+ margin-top: $header-height + 2;
+
+ .right-sidebar {
+ top: ($header-height * 2) + 2;
+ }
+}
+
+.activities {
+
+ .nav-block {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border-bottom: none;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index ae7bdf14c40..874416e1007 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -9,6 +9,10 @@
margin-top: -2px;
float: right;
}
+
+ .dropdown-menu-toggle {
+ line-height: 20px;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 6efc6ec1e4b..21d87cc9d34 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -8,7 +8,7 @@
background: #fff;
border-color: $input-border;
height: 35px;
- padding: $gl-vert-padding $gl-btn-padding;
+ padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
border-radius: $border-radius-base;
@@ -165,11 +165,6 @@
background-size: 16px 16px !important;
}
-/** Branch/tag selector **/
-.project-refs-form .select2-container {
- width: 160px !important;
-}
-
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index bd91f51708c..188823054fd 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,40 +1,32 @@
-#logo {
- z-index: 2;
- position: absolute;
- width: 58px;
- cursor: pointer;
- margin-top: 8px;
-}
-
.page-with-sidebar {
padding-top: $header-height;
- transition-duration: .3s;
+ padding-bottom: 25px;
+ transition: padding $sidebar-transition-duration;
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
- overflow-y: auto;
- overflow-x: hidden;
left: 0;
height: 100%;
- transition-duration: .3s;
- }
-
- .gitlab-text-container-link {
- z-index: 1;
- position: absolute;
- left: 0;
+ overflow: hidden;
+ transition: width $sidebar-transition-duration;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $background-color;
+
+ .nicescroll-rails-hr {
+ // TODO: Figure out why nicescroll doesn't hide horizontal bar
+ display: none!important;
+ }
}
.content-wrapper {
width: 100%;
+ transition: padding $sidebar-transition-duration;
.container-fluid {
background: #fff;
@@ -48,291 +40,147 @@
}
}
-.sidebar-wrapper {
- .header-logo {
- border-bottom: 1px solid transparent;
- float: left;
- height: $header-height;
- width: $sidebar_width;
- position: fixed;
- z-index: 999;
- overflow: hidden;
- transition-duration: .3s;
-
- a {
- float: left;
- height: $header-height;
- width: 100%;
- padding-left: 22px;
- overflow: hidden;
- outline: none;
- transition-duration: .3s;
-
- img {
- width: 36px;
- height: 36px;
- }
-
- #tanuki-logo, img {
- float: left;
- }
-
- .gitlab-text-container {
- width: 230px;
-
- h3 {
- width: 158px;
- float: left;
- margin: 0;
- margin-left: 50px;
- font-size: 19px;
- line-height: 50px;
- font-weight: normal;
- }
- }
- }
-
- &:hover {
- background-color: #eee;
- }
- }
-
- .sidebar-user {
- padding: 7px 22px;
- position: fixed;
- bottom: 40px;
- width: $sidebar_width;
- overflow: hidden;
- transition-duration: .3s;
-
- .username {
- margin-left: 10px;
- width: $sidebar_width - 2 * 10px;
- font-size: 16px;
- line-height: 34px;
- }
- }
-}
-
-
-.tanuki-shape {
- transition: all 0.8s;
+.nav-sidebar {
+ position: absolute;
+ top: 50px;
+ bottom: 0;
+ width: $sidebar_width;
+ overflow-y: auto;
+ overflow-x: hidden;
- &:hover, &.highlight {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
+ @media (min-width: $sidebar-breakpoint) {
+ bottom: 50px;
}
-}
-
-
-.nav-sidebar {
- margin-top: 14 + $header-height;
- margin-bottom: 100px;
- transition-duration: .3s;
- list-style: none;
- overflow: hidden;
&.navbar-collapse {
padding: 0 !important;
}
li {
- width: $sidebar_width;
-
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
+ .icon-container {
+ width: 34px;
+ display: inline-block;
+ text-align: center;
+ }
+
a {
- padding: 7px 15px;
+ padding: 7px 15px 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
- color: $gray;
display: block;
text-decoration: none;
- padding-left: 23px;
font-weight: normal;
outline: none;
- &:hover {
- text-decoration: none;
- }
-
- &:active, &:focus {
+ &:hover,
+ &:active,
+ &:focus {
text-decoration: none;
}
i {
- width: 16px;
- color: $gray-light;
- margin-right: 13px;
- }
-
- .count {
- float: right;
- background: #eee;
- padding: 0 8px;
- @include border-radius(6px);
+ font-size: 16px;
}
- &.back-link i {
- transition-duration: .3s;
+ i,
+ svg {
+ margin-right: 13px;
}
}
}
-}
-
-.sidebar-subnav {
- margin-left: 0;
- padding-left: 0;
- li {
- list-style: none;
+ .count {
+ float: right;
+ padding: 0 8px;
+ @include border-radius(6px);
}
}
-.collapse-nav a {
+.toggle-nav-collapse {
width: $sidebar_width;
- position: fixed;
- bottom: 0;
+ position: absolute;
+ top: 0;
left: 0;
- font-size: 13px;
- background: transparent;
- height: 40px;
- text-align: center;
- line-height: 40px;
+ min-height: 50px;
+ padding: 5px 0;
+ font-size: 18px;
+ line-height: 30px;
+}
+
+.nav-header-btn {
+ padding: 10px 5px;
+ color: inherit;
transition-duration: .3s;
- outline: none;
- &:hover {
+ &:hover,
+ &:focus {
+ color: $white-light;
text-decoration: none;
}
}
-.sidebar-wrapper {
- &.hidden-nav {
- width: 0;
- }
-}
-
-.page-sidebar-collapsed {
- padding-left: $sidebar_collapsed_width;
+.pin-nav-btn {
+ display: none;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ height: 50px;
+ width: $sidebar_width;
+ line-height: 30px;
- @media (max-width: $screen-xs-min) {
- padding-left: 0;
+ @media (min-width: $sidebar-breakpoint) {
+ display: block;
}
- .sidebar-wrapper {
- width: $sidebar_collapsed_width;
-
- @media (max-width: $screen-xs-min) {
- width: 0;
- }
-
- .header-logo {
- width: $sidebar_collapsed_width;
-
- @media (max-width: $screen-xs-min) {
- width: 0;
- }
-
- a {
- padding-left: ($sidebar_collapsed_width - 36) / 2;
-
- .gitlab-text-container {
- display: none;
- }
- }
- }
-
- .nav-sidebar {
- width: $sidebar_collapsed_width;
-
- li {
- width: auto;
-
- a {
- span {
- display: none;
- }
- }
- }
- }
-
- .collapse-nav a {
- width: $sidebar_collapsed_width;
-
- @media (max-width: $screen-xs-min) {
- width: 0;
- }
- }
-
- .sidebar-user {
- padding-left: ($sidebar_collapsed_width - 36) / 2;
- width: $sidebar_collapsed_width;
-
- @media (max-width: $screen-xs-min) {
- width: 0;
- padding-left: 0;
- padding-right: 0;
- }
-
- .username {
- display: none;
- }
- }
+ .fa {
+ transition: transform .15s;
}
- .layout-nav {
- padding-right: $sidebar_collapsed_width;
-
- @media (max-width: $screen-xs-min) {
- padding-right: 0;;
+ &.is-active {
+ .fa {
+ transform: rotate(90deg);
}
}
}
-.page-sidebar-expanded {
- padding-left: $sidebar_collapsed_width;
-
- @media (min-width: $screen-md-min) {
- padding-left: $sidebar_width;
- }
+.page-sidebar-collapsed {
+ padding-left: 0;
- @media (max-width: $screen-xs-min) {
- padding-left: 0;
+ .sidebar-wrapper {
+ width: 0;
}
+}
+.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
-
- .nav-sidebar {
- width: $sidebar_width;
- }
-
- .nav-sidebar li a {
- width: 230px;
-
- &.back-link {
- i {
- opacity: 0;
- }
- }
- }
}
+}
+.page-sidebar-pinned {
+ .content-wrapper,
.layout-nav {
- @media (max-width: $screen-xs-min) {
- padding-right: 0;;
+ @media (min-width: $sidebar-breakpoint) {
+ padding-left: $sidebar_width;
}
+ }
+}
- @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
- padding-right: 62px;
+header.header-pinned-nav {
+ @media (min-width: $sidebar-breakpoint) {
+ padding-left: ($sidebar_width + $gl-padding);
+
+ .side-nav-toggle {
+ display: none;
}
- @media (min-width: $screen-md-min) {
- padding-right: $sidebar_width;
+ .header-content {
+ padding-left: 0;
}
}
}
@@ -353,7 +201,9 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- padding-right: $sidebar_collapsed_width;
+ &:not(.build-sidebar) {
+ padding-right: $sidebar_collapsed_width;
+ }
}
@media (min-width: $screen-md-min) {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 29501069d27..0b0bd80c326 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,7 +5,7 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding $gl-btn-padding;
+ padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 6a45c34ccbb..e3154657c54 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -192,3 +192,8 @@
.text-info:hover {
color: $brand-info;
}
+
+// Prevent datetimes on tooltips to break into two lines
+.local-timeago {
+ white-space: nowrap;
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2779cd56788..3575984b229 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -269,3 +269,11 @@ h1, h2, h3, h4 {
text-align: right;
}
}
+
+.idiff.deletion {
+ background: $line-removed-dark;
+}
+
+.idiff.addition {
+ background: $line-added-dark;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 84e74db06b0..211a9af2348 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -2,17 +2,19 @@
* Layout
*/
$sidebar_collapsed_width: 62px;
-$sidebar_width: 230px;
+$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
+$sidebar-transition-duration: .15s;
+$sidebar-breakpoint: 1024px;
/*
* UI elements
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
-$table-border-color: #ececec;
+$table-border-color: #f0f0f0;
$background-color: #fafafa;
/*
@@ -57,13 +59,15 @@ $code_line_height: 1.5;
*/
$gl-padding: 16px;
$gl-btn-padding: 10px;
+$gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
/*
* Misc
*/
-$row-hover: #f4f8fe;
+$row-hover: #f7faff;
+$row-hover-border: #b2d7ff;
$progress-color: #c0392b;
$avatar_radius: 50%;
$header-height: 50px;
@@ -78,6 +82,9 @@ $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee;
$layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca;
+$btn-side-margin: 10px;
+$btn-sm-side-margin: 7px;
+$btn-xs-side-margin: 5px;
/*
* Color schema
@@ -104,7 +111,7 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
-$orange-light: rgba(252, 109, 38, 0.80);
+$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
@@ -119,8 +126,8 @@ $border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
$border-white-dark: #c6cacf;
-$border-gray-light: rgba(0, 0, 0, 0.06);
-$border-gray-normal: rgba(0, 0, 0, 0.10);;
+$border-gray-light: #dcdcdc;
+$border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf;
$border-green-light: #2faa60;
@@ -149,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
+/* tanuki logo colors */
+$tanuki-red: #e24329;
+$tanuki-orange: #fc6d26;
+$tanuki-yellow: #fca326;
+
/*
* State colors:
*/
@@ -178,6 +190,7 @@ $table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
+
/*
* Fonts
*/
@@ -254,3 +267,11 @@ $calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+
+/*
+ * Personal Access Tokens
+ */
+$personal-access-tokens-disabled-label-color: #bbb;
+
+$ci-output-bg: #1d1f21;
+$ci-text-color: #c5c8c6;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index f870ea0d87f..ff02ebdd34c 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -32,7 +32,7 @@
}
}
-.zen-cotrol {
+.zen-control {
padding: 0;
color: #555;
background: none;
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
new file mode 100644
index 00000000000..9495c5b3f37
--- /dev/null
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -0,0 +1,138 @@
+// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout
+// used for Devise email templates, and _should not_ be included in any
+// application stylesheets.
+//
+// Styles defined here are embedded directly into the resulting email HTML via
+// the `premailer` gem.
+
+$body-background-color: #363636;
+$message-background-color: #fafafa;
+
+$header-color: #6b4fbb;
+$body-color: #444;
+$cta-color: #e14329;
+$footer-link-color: #7e7e7e;
+
+$font-family: Helvetica, Arial, sans-serif;
+
+body {
+ background-color: $body-background-color;
+ font-family: $font-family;
+ margin: 0;
+ padding: 0;
+}
+
+table {
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+
+ border: 0;
+ border-collapse: separate;
+
+ &#wrapper {
+ background-color: $body-background-color;
+ width: 100%;
+ }
+
+ &#header {
+ margin: 0 auto;
+ text-align: left;
+ width: 600px;
+
+ & > td {
+ text-align: center;
+ }
+ }
+
+ &#body {
+ background-color: $message-background-color;
+ border: 1px solid #000;
+ border-radius: 4px;
+ margin: 0 auto;
+ width: 600px;
+ }
+
+ &#footer {
+ color: $footer-link-color;
+ font-size: 14px;
+ text-align: center;
+ width: 100%;
+ }
+
+ td {
+ &#body-container {
+ padding: 20px 40px;
+ }
+ }
+}
+
+.center {
+ text-align: center;
+}
+
+#logo {
+ border: none;
+ outline: none;
+ min-height: 88px;
+ width: 134px;
+}
+
+#content {
+ h2 {
+ color: $header-color;
+ font-size: 30px;
+ font-weight: 400;
+ line-height: 34px;
+ margin-top: 0;
+ }
+
+ p {
+ color: $body-color;
+ font-size: 17px;
+ line-height: 24px;
+ margin-bottom: 0;
+ }
+}
+
+#cta {
+ border: 1px solid $cta-color;
+ border-radius: 3px;
+ display: inline-block;
+ margin: 20px 0;
+ padding: 12px 24px;
+
+ a {
+ background-color: $message-background-color;
+ color: $cta-color;
+ display: inline-block;
+ text-decoration: none;
+ }
+}
+
+#tanuki {
+ padding: 40px 0 0;
+
+ img {
+ border: none;
+ outline: none;
+ width: 37px;
+ min-height: 36px;
+ }
+}
+
+#tagline {
+ font-size: 22px;
+ font-weight: 100;
+ padding: 4px 0 40px;
+}
+
+#social {
+ padding: 0 10px 20px;
+ width: 600px;
+ word-spacing: 20px;
+
+ a {
+ color: $footer-link-color;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
new file mode 100644
index 00000000000..7f645d3089d
--- /dev/null
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -0,0 +1,182 @@
+@import "framework/variables";
+
+// This file is largely copied from `highlight/white.scss`, but modified to
+// avoid all descendant selectors (`table td`). This is because the CSS inlining
+// we use performs dramatically worse on descendant selectors than the
+// alternatives.
+// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
+//
+// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
+// preference): plain class selectors, type (element name) selectors, or
+// explicit child selectors.
+
+table.code {
+ width: 100%;
+ font-family: monospace;
+ border: none;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+
+ > tr > td {
+ line-height: $code_line_height;
+ font-family: monospace;
+ font-size: $code_font_size;
+
+ &.diff-line-num {
+ margin: 0;
+ padding: 0;
+ border: none;
+ padding: 0 5px;
+ border-right: 1px solid;
+ text-align: right;
+ min-width: 35px;
+ max-width: 50px;
+ width: 35px;
+ }
+
+ &.line_content {
+ display: block;
+ margin: 0;
+ padding: 0 0.5em;
+ border: none;
+ white-space: pre;
+ }
+ }
+}
+
+.line-numbers, .diff-line-num {
+ background-color: $background-color;
+}
+
+.diff-line-num, .diff-line-num a {
+ color: $black-transparent;
+}
+
+pre.code, .diff-line-num {
+ border-color: $table-border-gray;
+}
+
+.code.white, pre.code, .line_content {
+ background-color: #fff;
+ color: #333;
+}
+
+.diff-line-num {
+ &.old {
+ background-color: $line-number-old;
+ border-color: $line-removed-dark;
+ }
+
+ &.new {
+ background-color: $line-number-new;
+ border-color: $line-added-dark;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-number-select;
+ border-color: $line-select-yellow-dark;
+ }
+}
+
+.line_content {
+ &.old {
+ background-color: $line-removed;
+
+ > .line > span.idiff, > .line > span > span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ > .line > span.idiff, > .line > span > span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ color: $black-transparent;
+ background-color: $match-line;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-select-yellow;
+ }
+}
+
+pre > .hll {
+ background-color: #f8eec7 !important;
+}
+
+span.highlight_word {
+ background-color: #fafe3d !important;
+}
+
+.hll { background-color: #f8f8f8 }
+.c { color: #998; font-style: italic; }
+.err { color: #a61717; background-color: #e3d2d2; }
+.k { font-weight: bold; }
+.o { font-weight: bold; }
+.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: #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: #a00; }
+.kc { font-weight: bold; }
+.kd { font-weight: bold; }
+.kn { font-weight: bold; }
+.kp { font-weight: bold; }
+.kr { font-weight: bold; }
+.kt { color: #458; font-weight: bold; }
+.m { color: #099; }
+.s { color: #d14; }
+.n { color: #333; }
+.na { color: teal; }
+.nb { color: #0086b3; }
+.nc { color: #458; font-weight: bold; }
+.no { color: teal; }
+.ni { color: purple; }
+.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: #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: #d14; }
+.ss { color: #990073; }
+.bp { color: #999; }
+.vc { color: teal; }
+.vg { color: teal; }
+.vi { color: teal; }
+.il { color: #099; }
+.gc { color: #999; background-color: #eaf2f5; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index 0a13a7e0b54..fc12964872d 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -6,19 +6,19 @@ p.details {
font-style: italic;
color: #777
}
-.footer p {
+.footer > p {
font-size: small;
color: #777
}
pre.commit-message {
white-space: pre-wrap;
}
-.file-stats a {
+.file-stats > a {
text-decoration: none;
-}
-.file-stats .new-file {
- color: #090;
-}
-.file-stats .deleted-file {
- color: #b00;
+ > .new-file {
+ color: #090;
+ }
+ > .deleted-file {
+ color: #b00;
+ }
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index e05f14e7496..1d34a7f79ae 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -71,3 +71,36 @@
@extend .broadcast-message;
margin-bottom: 20px;
}
+
+
+// Users List
+
+.users-list {
+ .user-row {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ }
+
+ .user-details {
+ flex: 1 1 auto;
+ }
+
+ .user-name {
+ display: inline-block;
+ font-weight: bold;
+ }
+
+ .controls {
+ > .btn, > .dropdown {
+ margin-left: 5px;
+ }
+ }
+
+ .dropdown {
+ .btn-block {
+ margin-bottom: 0;
+ line-height: inherit;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 37bf38fa65d..5faedfedd66 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,6 +1,4 @@
.awards {
- line-height: 34px;
-
.emoji-icon {
width: 20px;
height: 20px;
@@ -9,11 +7,10 @@
.emoji-menu {
position: absolute;
- top: 100%;
- left: 0;
margin-top: 3px;
- z-index: 1000;
- min-width: 160px;
+ padding: $gl-padding;
+ z-index: 9;
+ width: 300px;
font-size: 14px;
background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
@@ -23,7 +20,12 @@
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
- transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition: .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition-property: transform, opacity;
+
+ &.is-aligned-right {
+ transform-origin: 100% -45px;
+ }
&.is-visible {
pointer-events: all;
@@ -32,20 +34,18 @@
}
.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;
- }
}
}
+.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;
+}
+
.emoji-menu-list {
list-style: none;
padding-left: 0;
@@ -94,20 +94,30 @@
.award-control {
margin-right: 5px;
+ margin-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0;
+ &:hover,
&.active,
&:active {
- background-color: $white-dark;
+ background-color: $row-hover;
+ border-color: $row-hover-border;
box-shadow: none;
outline: 0;
}
+ &.btn {
+ &:focus {
+ outline: 0;
+ }
+ }
+
&.is-loading {
- .award-control-icon {
+ .award-control-icon-normal,
+ .emoji-icon {
display: none;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index aa41565f812..e8f1935d239 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -3,12 +3,7 @@
background: #111;
color: #fff;
font-family: $monospace_font;
- white-space: pre;
- white-space: pre-wrap; /* css-3 */
- white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
- white-space: -pre-wrap; /* Opera 4-6 */
- white-space: -o-pre-wrap; /* Opera 7 */
- word-wrap: break-word; /* Internet Explorer 5.5+ */
+ white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
@@ -58,37 +53,92 @@
left: 70px;
}
}
+}
- .build-widget {
- padding: 10px;
- background: $background-color;
- margin-bottom: 20px;
- border-radius: 4px;
+.build-header {
+ position: relative;
+ padding-right: 40px;
- .title {
- margin-top: 0;
- color: #666;
- line-height: 1.5;
- }
- .attr-name {
- color: #777;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
}
- .alert-disabled {
- background: $background-color;
+ a {
+ color: $gl-gray;
- a {
- color: #3084bb !important;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
}
}
+
+ code {
+ color: $code-color;
+ }
+
+ .avatar {
+ float: none;
+ margin-right: 2px;
+ margin-left: 2px;
+ }
}
table.builds {
-
.build-link {
a {
color: $gl-dark-link-color;
}
}
}
+
+.build-trace {
+ background: $ci-output-bg;
+ color: $ci-text-color;
+ white-space: pre;
+ overflow-x: auto;
+ font-size: 12px;
+
+ .fa-refresh {
+ font-size: 24px;
+ }
+
+ .bash {
+ display: block;
+ }
+}
+
+.right-sidebar.build-sidebar {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+
+ &.right-sidebar-collapsed {
+ display: none;
+ }
+
+ .block {
+ width: 100%;
+ }
+
+ .build-sidebar-header {
+ padding-top: 0;
+
+ .gutter-toggle {
+ margin-top: 0;
+ }
+ }
+}
+
+.build-detail-row {
+ margin-bottom: 5px;
+}
+
+.build-light-text {
+ color: $gl-placeholder-color;
+}
+
+.build-gutter-toggle {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ margin-top: -17px;
+}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index c2cd227571f..35ab28b3fea 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,8 +26,30 @@
.commit-info-row {
margin-bottom: 10px;
+ line-height: 24px;
+ padding-top: 6px;
+
+ &.commit-info-row-header {
+ line-height: 34px;
+
+ @media (min-width: $screen-sm-min) {
+ margin-bottom: 0;
+ }
+
+ .commit-options-dropdown-caret {
+ @media (max-width: $screen-sm) {
+ margin-left: 0;
+ }
+ }
+ }
+
.avatar {
@extend .avatar-inline;
+ margin-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 4px;
+ }
}
.commit-committer-link,
.commit-author-link {
@@ -35,10 +57,6 @@
font-weight: bold;
}
- .time_ago {
- margin-left: 8px;
- }
-
.fa-clipboard {
color: $dropdown-title-btn-color;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c8c6bbde084..85bbf70e188 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,84 +7,113 @@
margin-right: 9px;
}
-.lists-separator {
- margin: 10px 0;
- border-color: #ddd;
+.commit-header {
+ padding: 5px 10px;
+ background-color: $background-color;
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+ font-size: 14px;
+
+ &:first-child {
+ border-top-width: 0;
+ }
}
-.commits-row {
- ul {
- margin: 0;
+.commit-row-title {
+ line-height: 1;
+ margin-bottom: 7px;
- li.commit {
- padding: 8px 0;
- }
+ .notes_count {
+ float: right;
+ margin-right: 10px;
+ }
+
+ .str-truncated {
+ max-width: 70%;
}
- .commits-row-date {
- font-size: 15px;
- line-height: 20px;
- margin-bottom: 5px;
+ .commit-row-message {
+ color: $gl-dark-link-color;
+ }
+
+ .text-expander {
+ display: inline-block;
+ background: $gray-light;
+ color: $gl-placeholder-color;
+ padding: 0 5px;
+ cursor: pointer;
+ border: 1px solid $border-gray-dark;
+ border-radius: $border-radius-default;
+ margin-left: 5px;
+
+ &:hover {
+ background-color: darken($gray-light, 10%);
+ text-decoration: none;
+ }
}
}
-li.commit {
- list-style: none;
+.commit-actions {
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ margin-left: $gl-padding;
+ margin-top: 2px;
+ font-size: 0;
+ }
- .commit-row-title {
- font-size: $list-font-size;
- line-height: 20px;
- margin-bottom: 2px;
+ .btn-transparent {
+ padding-left: 0;
+ padding-right: 0;
+ }
- .btn-clipboard {
- margin-top: -1px;
+ .btn {
+ &:not(:first-child) {
+ margin-left: $gl-padding;
}
+ }
+}
- .notes_count {
- float: right;
- margin-right: 10px;
- }
+.commit-short-id {
+ font-family: $monospace_font;
+ font-weight: 600;
+}
- .commit_short_id {
- min-width: 65px;
- color: $gl-dark-link-color;
- font-family: $monospace_font;
- }
+.commit {
+ padding: 10px 0;
+ position: relative;
- .str-truncated {
- max-width: 70%;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-left: 46px;
+ }
- .commit-row-message {
- color: $gl-dark-link-color;
+ &:not(:last-child) {
+ border-bottom: 1px solid #eee;
+ }
- &:hover {
- text-decoration: underline;
- }
- }
+ a,
+ button {
+ color: $gl-dark-link-color;
+ vertical-align: baseline;
+ }
- .text-expander {
- background: #eee;
- color: #555;
- padding: 0 5px;
- cursor: pointer;
- margin-left: 4px;
- &:hover {
- background-color: #ddd;
- }
- }
+
+ .avatar {
+ margin-left: -46px;
}
.item-title {
display: inline-block;
- max-width: 70%;
+
+ @media (min-width: $screen-sm-min) {
+ max-width: 70%;
+ }
}
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
padding: 10px 15px;
- margin: 5px 0 10px 5px;
+ margin: 10px 0;
background: #f9f9f9;
display: none;
@@ -93,6 +122,7 @@ li.commit {
background: inherit;
padding: 0;
margin: 0;
+ white-space: pre-wrap;
}
a {
@@ -102,7 +132,7 @@ li.commit {
.commit-row-info {
color: $gl-gray;
- line-height: 24px;
+ line-height: 1;
a {
color: $gl-gray;
@@ -111,10 +141,6 @@ li.commit {
.avatar {
margin-right: 8px;
}
-
- .committed_ago {
- display: inline-block;
- }
}
&.inline-commit {
diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss
index 125f495d6d4..292225c5261 100644
--- a/app/assets/stylesheets/pages/confirmation.scss
+++ b/app/assets/stylesheets/pages/confirmation.scss
@@ -2,13 +2,21 @@
margin-bottom: 20px;
border-bottom: 1px solid #eee;
- > h1 {
+ > h1, h2, h3, h4, h5, h6 {
font-weight: 400;
}
.lead {
margin-bottom: 20px;
}
+
+ ul, ol {
+ padding-left: 0;
+ }
+
+ li {
+ list-style-type: none;
+ }
}
.confirmation-content {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 5e61e61d85c..1b389d83525 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -29,8 +29,6 @@
margin-top: 6px;
p {
- overflow-x: auto;
-
&:last-child {
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1a7d5f9666e..21b1c223c88 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -4,6 +4,11 @@
margin-bottom: $gl-padding;
border-radius: 3px;
+ .commit-short-id {
+ font-family: $regular_font;
+ font-weight: 400;
+ }
+
.diff-header {
position: relative;
background: $background-color;
@@ -429,13 +434,3 @@
}
}
}
-
-.discussion {
- .diff-content {
- .diff-line-num {
- &:before {
- content: attr(data-linenumber);
- }
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 8981f070a20..1aa4e06d975 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -23,7 +23,7 @@
.file-title {
@extend .monospace;
- line-height: 42px;
+ line-height: 35px;
padding-top: 7px;
padding-bottom: 7px;
@@ -43,7 +43,7 @@
.editor-file-name {
@extend .monospace;
-
+
float: left;
margin-right: 10px;
}
@@ -59,7 +59,28 @@
}
.encoding-selector,
- .license-selector {
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
display: inline-block;
+ vertical-align: top;
+ font-family: $regular_font;
+ }
+
+ .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
+ .dropdown {
+ line-height: 21px;
+ }
+
+ .dropdown-menu-toggle {
+ vertical-align: top;
+ width: 220px;
+ }
+ }
+
+ .gitlab-ci-yml-selector {
+ .dropdown-menu-toggle {
+ width: 250px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
new file mode 100644
index 00000000000..e160d676e35
--- /dev/null
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -0,0 +1,5 @@
+.environments {
+ .commit-title {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 6fe57c737b3..a2145956eb5 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -54,6 +54,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
@@ -136,9 +140,10 @@
.event-last-push {
overflow: auto;
width: 100%;
+
.event-last-push-text {
@include str-truncated(100%);
- padding: 5px 0;
+ padding: 4px 0;
font-size: 13px;
float: left;
margin-right: -150px;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ec6c099df5b..701b9388454 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -38,4 +38,53 @@
margin-right: 15px;
}
}
+
+ &.group-admin {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+
+ .group-avatar, .group-details, .group-controls {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ }
+
+ .group-details {
+ flex: 1 1 auto;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .group-controls {
+ align-items: center;
+
+ a {
+ margin-left: 5px;
+ }
+ }
+ }
+
+}
+
+.ldap-group-links {
+ .form-actions {
+ margin-bottom: $gl-padding;
+ }
+}
+
+.groups-cover-block {
+ .container-fluid {
+ position: relative;
+ }
+
+ .group-right-buttons {
+ position: absolute;
+ right: 16px;
+ .btn {
+ @include btn-gray;
+ padding: 3px 10px;
+ background-color: $background-color;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 4a95b7b852e..00ab42bec5c 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -57,4 +57,12 @@
.documentation {
padding: 7px;
+
+ // Border around images in the help pages.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ max-height: calc(100vh - 100px);
+ }
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 1cf3023ecc9..542fa244689 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -4,6 +4,14 @@
margin-right: 1px;
}
}
+
+ // Border around images in issue and MR descriptions.
+ .description img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ max-height: calc(100vh - 100px);
+ }
}
.issuable-filter-count {
@@ -29,11 +37,15 @@
}
}
-.issuable-sidebar {
+.right-sidebar {
a {
color: inherit;
}
+ .issuable-header-text {
+ margin-top: 7px;
+ }
+
.block {
@include clearfix;
padding: $gl-padding 0;
@@ -60,10 +72,6 @@
margin-top: 0;
}
- .issuable-count {
- margin-top: 7px;
- }
-
.gutter-toggle {
margin-left: 20px;
padding-left: 10px;
@@ -74,6 +82,10 @@
}
}
+ .block-first {
+ padding-top: 0;
+ }
+
.title {
color: $gl-text-color;
margin-bottom: 10px;
@@ -125,7 +137,7 @@
.right-sidebar {
position: fixed;
- top: 58px;
+ top: $header-height;
bottom: 0;
right: 0;
z-index: 10;
@@ -141,7 +153,6 @@
.assign-yourself {
margin-top: 10px;
- font-weight: normal;
display: block;
}
}
@@ -150,6 +161,14 @@
font-weight: 600;
}
+ .light {
+ font-weight: normal;
+ }
+
+ .no-value {
+ color: $gl-placeholder-color;
+ }
+
.sidebar-collapsed-icon {
display: none;
}
@@ -240,11 +259,16 @@
padding-bottom: 0;
margin-bottom: 10px;
}
+
+ .issuable-header-btn {
+ display: none;
+ }
}
- .issuable-pager {
+ .issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
+
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
@@ -255,7 +279,7 @@
}
}
- a:not(.issuable-pager) {
+ a {
&:hover {
color: $md-link-color;
text-decoration: none;
@@ -314,7 +338,7 @@
margin-left: 5px;
a {
- color: #8c8c8c;
+ color: $gl-placeholder-color;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index fc9db97132d..4e35ca329e4 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -40,11 +40,6 @@
}
}
-.issue-search-form {
- margin: 0;
- height: 24px;
-}
-
form.edit-issue {
margin: 0;
}
@@ -96,8 +91,3 @@ form.edit-issue {
.issue-form .select2-container {
width: 250px !important;
}
-
-.issue-closed-by-widget {
- color: $gl-text-color;
- margin-left: 52px;
-}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index e179bdf0048..47bfd144930 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -6,6 +6,7 @@
height: 30px;
display: inline-block;
margin-right: 10px;
+ margin-bottom: 10px;
}
&.suggest-colors-dropdown {
@@ -51,10 +52,25 @@
.label-row {
.label-name {
display: inline-block;
- width: 200px;
+ margin-bottom: 10px;
- @media (max-width: $screen-xs-min) {
- display: block;
+ @media (min-width: $screen-sm-min) {
+ width: 200px;
+ margin-bottom: 0;
+ }
+ }
+
+ .label-description {
+ display: block;
+ margin-bottom: 10px;
+ margin-left: 50px;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ width: 40%;
+ margin-left: 10px;
+ margin-bottom: 0;
+ vertical-align: middle;
}
}
@@ -68,10 +84,6 @@
padding: 3px 4px;
}
-.label-subscription {
- display: inline-block;
-}
-
.dropdown-labels-error {
padding: 5px 10px;
margin-bottom: 10px;
@@ -79,62 +91,109 @@
color: $white-light;
}
-@mixin labels-mobile {
- @media (max-width: $screen-xs-min) {
- display: block;
- width: 100%;
- margin-left: 0;
- padding: 10px 0;
+.manage-labels-list {
+ .btn-action {
+ color: $gl-dark-link-color;
+
+ .fa {
+ font-size: 18px;
+ vertical-align: middle;
+ }
+
+ &:hover {
+ color: $gl-link-color;
+
+ &.remove-row {
+ color: $gl-danger;
+ }
+ }
}
-}
+ .dropdown {
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+ }
+}
-.manage-labels-list {
+.draggable-handler {
+ display: inline-block;
+ opacity: 0;
+ transition: opacity .3s;
+ color: $gray-darkest;
+}
- .prepend-left-10, .prepend-description-left {
- display: inline-block;
- width: 40%;
- vertical-align: middle;
+.prioritized-labels {
+ margin-bottom: 30px;
- @include labels-mobile;
+ .add-priority {
+ display: none;
+ color: $gray-light;
}
- .prepend-description-left {
- width: 57%;
+ li:hover {
+ .draggable-handler {
+ display: inline-block;
+ opacity: 1;
+ }
+ }
+}
- @include labels-mobile;
+.other-labels {
+ .remove-priority {
+ display: none;
}
+}
- .pull-info-right {
- float: right;
+.toggle-priority {
+ display: inline-block;
+ vertical-align: middle;
- @media (max-width: $screen-xs-min) {
- float: none;
- }
+ button {
+ border-color: transparent;
+ padding: 5px 8px;
+ vertical-align: top;
+ font-size: 14px;
- .action-buttons {
+ &:hover {
border-color: transparent;
- padding: 6px;
- color: $gl-text-color;
-
- &.label-subscribe-button {
- padding-left: 0;
- }
}
+ }
+}
- i {
- color: $gl-text-color;
+.filtered-labels {
+ .label-row {
+ &:not(:last-child) {
+ margin-right: 5px;
}
+ }
- .append-right-20 {
- a {
- color: $gl-text-color;
- }
+ .label-remove {
+ border-left: 1px solid rgba(0, 0, 0, .1);
+ z-index: 3;
+ }
- @media (max-width: $screen-xs-min) {
- display: block;
- margin-bottom: 10px;
- }
+ .btn {
+ color: inherit;
+ }
+}
+
+.label-options-toggle {
+ width: 100%;
+}
+
+.label-subscribe-button {
+ .label-subscribe-button-loading {
+ display: none;
+ }
+
+ &.disabled {
+ .label-subscribe-button-icon {
+ display: none;
+ }
+
+ .label-subscribe-button-loading {
+ display: block;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c4005ba1e69..d9756b66af0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -41,7 +41,7 @@
margin: 0;
margin-left: 20px;
padding: 5px;
- padding-top: 12px;
+ padding-top: 8px;
line-height: 20px;
&.right {
@@ -79,11 +79,14 @@
}
&.ci-failed,
- &.ci-canceled,
&.ci-error {
color: $gl-danger;
}
+ &.ci-canceled {
+ color: $gl-gray;
+ }
+
a.monospace {
color: inherit;
}
@@ -105,11 +108,48 @@
font-size: 17px;
margin: 5px 0;
color: $gl-gray-dark;
+
+ &.has-conflicts .fa-exclamation-triangle {
+ color: $gl-warning;
+ }
+
}
p:last-child {
margin-bottom: 0;
}
+
+ .btn-grouped {
+ margin-left: 0;
+ margin-right: 7px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ h4 {
+ font-size: 15px;
+ }
+
+ p {
+ font-size: 13px;
+ }
+
+ .btn,
+ .btn-group,
+ .accept-action {
+ margin-bottom: 4px;
+ }
+
+ .accept-action {
+ width: 100%;
+ text-align: center;
+ }
+
+ .accept-control {
+ width: 100%;
+ text-align: center;
+ margin: 0;
+ }
+ }
}
.mr-widget-footer {
@@ -127,7 +167,8 @@
.commit {
margin: 0;
- padding: 2px 0;
+ padding-top: 2px;
+ padding-bottom: 2px;
list-style: none;
&:hover {
background: none;
@@ -213,6 +254,10 @@
.panel-footer {
padding: 5px 10px;
+
+ .btn {
+ min-width: auto;
+ }
}
.commit {
@@ -220,10 +265,15 @@
margin-bottom: 4px;
}
+ .item-title {
+ @media (min-width: $screen-sm-min) {
+ width: 49%;
+ }
+ }
+
.avatar {
- width: 20px;
- height: 20px;
- margin-right: 5px;
+ left: 0;
+ top: 2px;
}
.commit-row-info {
@@ -251,7 +301,7 @@
margin-bottom: 0;
}
- @media (min-width: $screen-sm-min) {
+ @media (min-width: $screen-xs-min) {
float: left;
width: 50%;
margin-bottom: 0;
@@ -280,11 +330,15 @@
background-color: $white-light;
color: $gl-placeholder-color;
}
+ }
+}
- th,
- td {
- padding: 16px;
+.merged-buttons {
+ .btn {
+ float: left;
+
+ &:not(:last-child) {
+ margin-right: 10px;
}
}
}
-
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 7fa13e66b43..3784010348a 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -87,6 +87,39 @@
}
}
+.md-header .nav-links {
+ display: flex;
+ display: -webkit-flex;
+ flex-flow: row wrap;
+ -webkit-flex-flow: row wrap;
+ width: 100%;
+
+ .pull-right {
+ // Flexbox quirk to make sure right-aligned items stay right-aligned.
+ margin-left: auto;
+ }
+}
+
+.confidential-issue-warning {
+ background-color: $gray-normal;
+ border-radius: 3px;
+ padding: 3px 12px;
+ margin: auto;
+ margin-top: 0;
+ text-align: center;
+ font-size: 13px;
+
+ @media (max-width: $screen-md-min) {
+ // On smaller devices the warning becomes the fourth item in the list,
+ // rather than centering, and grows to span the full width of the
+ // comment area.
+ order: 4;
+ -webkit-order: 4;
+ margin: 6px auto;
+ width: 100%;
+ }
+}
+
.discussion-form {
padding: $gl-padding-top $gl-padding;
background-color: $white-light;
@@ -96,17 +129,8 @@
display: none;
font-size: 15px;
- .form-actions {
- padding-left: 20px;
-
- .btn-save {
- float: left;
- }
-
- .note-form-option {
- float: left;
- padding: 2px 0 0 25px;
- }
+ .md-area {
+ background-color: #fff;
}
}
@@ -155,6 +179,10 @@
border-top: 1px solid $border-color;
}
+.md-helper {
+ padding-top: 10px;
+}
+
.toolbar-button {
padding: 0;
background: none;
@@ -195,3 +223,16 @@
float: left;
}
}
+
+.note-form-actions {
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ float: none;
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 624c8249f7e..ac8c02b59dc 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -41,6 +41,10 @@ ul.notes {
.timeline-icon {
.avatar {
visibility: hidden;
+
+ .discussion-body & {
+ visibility: visible;
+ }
}
}
}
@@ -69,6 +73,10 @@ ul.notes {
.note-edit-form {
display: block;
+
+ &.current-note-edit-form + .note-awards {
+ display: none;
+ }
}
}
@@ -80,24 +88,14 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // Reset ul style types since we're nested inside a ul already
+ @include bulleted-list;
+
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
}
- // Reset ul style types since we're nested inside a ul already
- & > ul {
- list-style-type: disc;
-
- ul {
- list-style-type: circle;
-
- ul {
- list-style-type: square;
- }
- }
- }
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -113,11 +111,58 @@ ul.notes {
code {
word-break: keep-all;
}
+
+ // Border around images in issue and MR comments.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px 0;
+ max-height: calc(100vh - 100px);
+ }
+ }
+ }
+
+ .note-awards {
+ .js-awards-block {
+ padding: 2px;
+ margin-top: 10px;
+ }
+
+ .award-control {
+ font-size: 13px;
+ padding: 2px 5px;
}
}
.note-header {
padding-bottom: 3px;
+ padding-right: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ .inline {
+ display: block;
+ }
+ }
+ }
+
+ .note-emoji-button {
+ .fa-spinner {
+ display: none;
+ }
+
+ &.is-loading {
+ .fa-smile-o {
+ display: none;
+ }
+
+ .fa-spinner {
+ display: inline-block;
+ }
+ }
}
}
@@ -179,6 +224,8 @@ ul.notes {
.discussion-header,
.note-header {
+ position: relative;
+
a {
color: inherit;
@@ -215,6 +262,20 @@ ul.notes {
color: $notes-action-color;
}
+.note-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ .note-action-button {
+ margin-left: 10px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ position: relative;
+ }
+}
+
.discussion-actions {
@media (max-width: $screen-md-max) {
float: none;
@@ -226,11 +287,15 @@ ul.notes {
}
}
-.note-action-button,
-.discussion-action-button {
+.note-action-button {
display: inline-block;
- margin-left: 10px;
- line-height: 24px;
+ margin-left: 0;
+ line-height: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 10px;
+ line-height: 24px;
+ }
.fa {
color: $notes-action-color;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
new file mode 100644
index 00000000000..6128868b670
--- /dev/null
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -0,0 +1,24 @@
+.pipelines {
+ .stage {
+ max-width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .duration, .finished_at {
+ margin: 4px 0;
+ }
+
+ .commit-title {
+ margin: 0;
+ }
+
+ .controls {
+ white-space: nowrap;
+ }
+
+ .btn {
+ margin: 4px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 843379a3f54..46371ec6871 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -66,12 +66,6 @@
}
}
-.calendar-hint {
- margin-top: -12px;
- float: right;
- font-size: 12px;
-}
-
.profile-link-holder {
display: inline;
@@ -134,14 +128,6 @@
}
}
-.change-username-title {
- color: $gl-warning;
-}
-
-.remove-account-title {
- color: $gl-danger;
-}
-
.provider-btn-group {
display: inline-block;
margin-right: 10px;
@@ -206,6 +192,25 @@
}
}
+.personal-access-tokens-never-expires-label {
+ color: $personal-access-tokens-disabled-label-color;
+}
+
+.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
+ text-align: center;
+}
+
+.created-personal-access-token-container {
+ #created-personal-access-token {
+ width: 90%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+}
+
.user-profile {
@media (max-width: $screen-xs-max) {
.cover-block {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c20f04653fc..bce4aac3334 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -5,61 +5,110 @@
font-weight: normal;
}
}
+
.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
- margin-bottom: 16px;
+ margin-bottom: 0;
}
+
.new_project,
-.edit_project {
- fieldset.features {
- .control-label {
+.edit-project {
+ fieldset {
+ &.features .control-label {
font-weight: normal;
}
+ .form-group {
+ margin-bottom: 5px;
+ }
+ &> .form-group {
+ padding-left: 0;
+ }
+ }
+ .help-block {
+ margin-bottom: 10px;
+ }
+ .project-path {
+ padding-right: 0;
+ .form-control {
+ border-radius: $border-radius-base;
+ }
+ }
+ .input-group > div {
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+ @media (max-width: $screen-xs-max) {
+ .input-group > div {
+ margin-bottom: 14px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ fieldset > .form-group:first-child {
+ padding-right: 0;
+ }
}
-}
-.project-name-holder {
- .help-inline {
- vertical-align: top;
- padding: 7px;
+ .input-group-addon {
+ &.static-namespace {
+ height: 35px;
+ border-radius: 3px;
+ border: 1px solid #e5e5e5;
+ }
+ &+ .select2 a {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
}
}
.project-home-panel {
- padding-bottom: 40px;
- border-bottom: 1px solid $border-color;
+ padding-top: 24px;
+ padding-bottom: 24px;
- .cover-controls {
- .project-settings-dropdown {
- margin-left: 10px;
- display: inline-block;
+ @media (min-width: $screen-sm-min) {
+ border-bottom: 1px solid $border-color;
+ }
- .dropdown-menu {
- left: auto;
- width: auto;
- right: 0;
- max-width: 240px;
- }
+ .project-avatar {
+ float: none;
+ margin-left: auto;
+ margin-right: auto;
+
+ &.identicon {
+ border-radius: 50%;
}
}
- .project-identicon-holder {
- margin-bottom: 16px;
-
- .avatar, .identicon {
- margin: 0 auto;
- float: none;
+ .project-title {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 1;
+
+ .fa {
+ margin-left: 2px;
+ font-size: 12px;
+ vertical-align: middle;
}
+ }
- .identicon {
- @include border-radius(50%);
+ .project-home-desc {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 15px;
+ max-width: 480px;
+
+ > p {
+ margin-bottom: 0;
}
}
.notifications-btn {
- margin-top: -28px;
-
- .fa-bell {
+ .fa-bell,
+ .fa-spinner {
margin-right: 6px;
}
@@ -67,91 +116,105 @@
margin-left: 6px;
}
}
+}
- .project-repo-buttons {
- margin-top: 20px;
- margin-bottom: 0;
+.project-repo-buttons {
+ font-size: 0;
- .count-buttons {
- display: block;
- margin-bottom: 20px;
+ .btn {
+ @include btn-gray;
+ padding: 3px 10px;
+
+ .fa {
+ color: $layout-link-gray;
+ }
+
+ .fa-caret-down {
+ margin-left: 3px;
}
+ }
+
+ .project-repo-btn-group,
+ .notification-dropdown {
+ margin-left: 10px;
+ }
+
+ .count-buttons {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .project-clone-holder {
+ display: inline-block;
- .clone-row {
- .split-repo-buttons,
- .project-clone-holder {
+ input {
+ height: 29px;
+ }
+ }
+
+ .count-with-arrow {
+ display: inline-block;
+ position: relative;
+ margin-left: 4px;
+
+ .arrow {
+ &:before {
+ content: '';
display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 50%;
+ left: 0;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: #dce0e5;
+ pointer-events: none;
}
- .split-repo-buttons {
- margin: 0 12px;
+ &:after {
+ content: '';
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 50%;
+ left: 1px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: #fff;
+ pointer-events: none;
}
}
-
- .btn {
+ .count {
@include btn-gray;
- text-transform: none;
- }
-
- .count-with-arrow {
display: inline-block;
- position: relative;
- margin-left: 4px;
-
- .arrow {
- &:before {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 0;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: #dce0e5;
- }
-
- &:after {
- content: '';
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 50%;
- left: 1px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: #fff;
- }
+ background: white;
+ border-radius: 2px;
+ border-width: 1px;
+ border-style: solid;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 13px;
+ padding: $gl-vert-padding $gl-padding;
+ letter-spacing: .4px;
+ padding: 7px 14px;
+ text-align: center;
+ vertical-align: middle;
+ touch-action: manipulation;
+ background-image: none;
+ white-space: nowrap;
+ margin: 0 10px 0 4px;
+
+ a {
+ color: inherit;
}
- .count {
- @include btn-gray;
- display: inline-block;
- background: white;
- border-radius: 2px;
- border-width: 1px;
- border-style: solid;
- font-size: 13px;
- font-weight: 600;
- line-height: 13px;
- padding: $gl-vert-padding $gl-padding;
- letter-spacing: .4px;
- padding: 10px 14px;
- text-align: center;
- vertical-align: middle;
- touch-action: manipulation;
- cursor: pointer;
- background-image: none;
- white-space: nowrap;
- margin: 0 11px 0 4px;
-
- &:hover {
- background: #fff;
- }
+
+ &:hover {
+ background: #fff;
}
}
}
@@ -161,7 +224,7 @@
display: inline-table;
margin-right: 12px;
- a {
+ > a {
margin: -1px;
}
}
@@ -194,10 +257,6 @@
color: #555;
}
-.project_member_row form {
- margin: 0;
-}
-
.transfer-project .select2-container {
min-width: 200px;
}
@@ -279,50 +338,82 @@ a.deploy-project-label {
}
}
-.project-import .btn {
- float: left;
- margin-right: 10px;
+.project-import {
+ .form-group {
+ margin-bottom: 0;
+ }
+ .import-buttons {
+ padding-left: 0;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ .btn {
+ margin-right: 10px;
+ padding: 8px 12px;
+ }
+ &> div {
+ margin-bottom: 14px;
+ padding-left: 0;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
}
.project-stats {
- text-align: center;
- margin-top: $gl-padding;
- margin-bottom: 0;
- padding-top: 10px;
- padding-bottom: 4px;
+ font-size: 0;
+ border-bottom: 1px solid $border-color;
- ul.nav {
- display: inline-block;
+ .nav {
+ padding-top: 12px;
+ padding-bottom: 12px;
}
- .nav li {
- display: inline;
+ .nav > li {
+ display: inline-block;
+
+ &:not(:last-child) {
+ margin-right: $gl-padding;
+ }
+
+ &.project-repo-buttons-right {
+ margin-top: 10px;
+
+ @media (min-width: $screen-md-min) {
+ float: right;
+ margin-top: 0;
+ }
+ }
}
.nav > li > a {
- @include btn-default;
- @include btn-gray;
-
+ padding: 0;
background-color: transparent;
- border: 1px solid #f7f8fa;
- margin-left: 12px;
- }
+ font-size: 15px;
+ line-height: 29px;
+ color: $notes-light-color;
- li {
- display: inline;
+ &:hover,
+ &:focus {
+ color: darken($notes-light-color, 15%);
+ }
}
- a {
- float: left;
- font-size: 17px;
- }
+ li.missing {
+ border: 1px dashed $border-gray-light;
+ border-radius: $border-radius-default;
- li.missing a {
- color: #5a6069;
- border: 1px dashed #dce0e5;
+ a {
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $notes-light-color;
+ display: block;
+ }
&:hover {
- background-color: #f0f2f5;
+ background-color: $gray-normal;
}
}
}
@@ -384,10 +475,6 @@ pre.light-well {
a:hover {
text-decoration: none;
}
-
- > span {
- margin-left: 10px;
- }
}
}
@@ -402,15 +489,42 @@ pre.light-well {
margin: 0;
}
-.project-show-activity {
- .activity-filter-block {
- margin-top: -1px;
+
+.activity-filter-block {
+ .controls {
+ padding-bottom: 7px;
+ margin-top: 8px;
+ border-bottom: 1px solid $border-color;
}
}
.project-last-commit {
+ @media (min-width: $screen-sm-min) {
+ margin-top: $gl-padding;
+ }
+
+ &.container-fluid {
+ padding-top: 12px;
+ padding-bottom: 12px;
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ border-right-width: 0;
+ border-left-width: 0;
+
+ @media (min-width: $screen-sm-min) {
+ border-right-width: 1px;
+ border-left-width: 1px;
+ }
+ }
+
+ &.container-limited {
+ @media (min-width: 1281px) {
+ border-radius: $border-radius-base;
+ }
+ }
+
.ci-status {
- margin-right: 16px;
+ margin-right: $gl-padding;
}
.commit-row-message {
@@ -418,19 +532,12 @@ pre.light-well {
}
.commit_short_id {
- margin: 0 5px;
+ margin-right: 5px;
color: $gl-link-color;
font-weight: 600;
}
.commit-author-link {
- margin-left: 7px;
- text-decoration: none;
- .avatar {
- float: none;
- margin-right: 4px;
- }
-
.commit-author-name {
font-weight: 600;
}
@@ -442,21 +549,21 @@ pre.light-well {
border-top: 0;
.edit-project-readme {
- z-index: 100;
+ z-index: 2;
position: relative;
}
+
+ .wiki h1 {
+ border-bottom: none;
+ padding: 0;
+ }
}
.git-clone-holder {
- width: 498px;
+ width: 380px;
.btn-clipboard {
border: 1px solid $border-color;
- padding: 6px $gl-padding;
- }
-
- .project-home-dropdown + & {
- margin-right: 45px;
}
.clone-options {
@@ -502,3 +609,26 @@ pre.light-well {
}
}
}
+
+.custom-notifications-form {
+ .is-loading {
+ .custom-notification-event-loading {
+ display: inline-block;
+ }
+ }
+}
+
+.custom-notification-event-loading {
+ display: none;
+ margin-left: 5px;
+
+ &.is-done {
+ color: $gl-text-green;
+ }
+}
+
+.project-refs-form {
+ .dropdown-menu {
+ width: 300px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 2bff70c8c64..9e9b18fdbb8 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -28,6 +28,7 @@
}
.search-input {
+ padding-right: 20px;
border: none;
font-size: 14px;
outline: none;
@@ -47,6 +48,7 @@
display: inline-block;
background-color: $location-badge-bg;
vertical-align: top;
+ cursor: default;
}
.search-input-container {
@@ -55,7 +57,7 @@
position: relative;
}
- .search-location-badge, .search-input-wrap {
+ .search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
}
@@ -156,13 +158,11 @@
.search-holder {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
- display: -ms-flexbox;
display: flex;
}
.search-field-holder {
-webkit-flex: 1 0 auto;
- -ms-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
margin-right: 0;
@@ -208,7 +208,7 @@
margin-top: 5px;
@media (min-width: $screen-sm-min) {
- width: 160px;
+ width: 180px;
margin-top: 0;
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 3fb70085713..2e8f356298d 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -12,3 +12,11 @@
border: 1px solid $warning-message-border;
border-radius: $border-radius-base;
}
+
+.warning-title {
+ color: $gl-warning;
+}
+
+.danger-title {
+ color: $gl-danger;
+}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 639d639d5b0..2aa939b7dc3 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -16,19 +16,6 @@
}
}
-.snippet-box {
- @include border-radius(2px);
-
- display: block;
- float: left;
- padding: 0 $gl-padding;
- font-weight: normal;
- margin-right: 10px;
- font-size: $gl-font-size;
- border: 1px solid;
- line-height: 32px;
-}
-
.markdown-snippet-copy {
position: fixed;
top: -10px;
@@ -36,3 +23,34 @@
max-height: 0;
max-width: 0;
}
+
+.file-holder.snippet-file-content {
+ padding-bottom: $gl-padding;
+ border-bottom: 1px solid $border-color;
+
+ .file-title {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+
+ .file-actions {
+ top: 12px;
+ }
+
+ .file-content {
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ }
+}
+
+.snippet-title {
+ font-size: 24px;
+ font-weight: normal;
+}
+
+.snippet-actions {
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ }
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 85a0304196c..69288b31cc4 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -14,24 +14,38 @@
font-size: 10px;
}
+#contributors-master {
+ @include make-md-column(12);
+
+ svg {
+ width: 100%;
+ }
+}
+
#contributors {
.contributors-list {
margin: 0 0 10px;
list-style: none;
padding: 0;
+
+ svg {
+ width: 100%;
+ }
}
.person {
- &:nth-child(even) {
- float: right;
- }
- float: left;
+ @include make-md-column(6);
margin-top: 10px;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.person .spark {
display: block;
background: #f3f3f3;
+ width: 100%;
}
.person .area-contributor {
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index e51c3491dae..cf16d070cfe 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -29,6 +29,17 @@
.todo-item {
.todo-title {
@include str-truncated(calc(100% - 174px));
+ overflow: visible;
+ }
+
+ .status-box {
+ margin: 0;
+ float: none;
+ display: inline-block;
+ font-weight: normal;
+ padding: 0 5px;
+ line-height: inherit;
+ font-size: 14px;
}
.todo-body {
@@ -51,6 +62,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
@@ -76,12 +91,11 @@
@media (max-width: $screen-xs-max) {
.todo-item {
- padding-left: $gl-padding;
-
.todo-title {
white-space: normal;
overflow: visible;
max-width: 100%;
+ margin-bottom: 10px;
}
.avatar {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index a84fc2e0318..5b61270daa8 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -15,16 +15,23 @@
margin-bottom: 0;
tr {
- > td, > th {
+ border-bottom: 1px solid $table-border-gray;
+ border-top: 1px solid $table-border-gray;
+
+ td, th {
line-height: 23px;
}
&:hover {
+ cursor: pointer;
+
td {
- background: $row-hover;
+ background-color: $row-hover;
+ border-top: 1px solid $row-hover-border;
+ border-bottom: 1px solid $row-hover-border;
}
- cursor: pointer;
}
+
&.selected {
td {
background: $gray-dark;
@@ -94,7 +101,8 @@
margin: 0;
.commit {
- padding: 0;
+ padding-top: 0;
+ padding-bottom: 0;
.commit-row-title {
.commit-row-message {
@@ -122,4 +130,6 @@
.tree-controls {
float: right;
margin-top: 11px;
+ position: relative;
+ z-index: 2;
}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 3f28e402929..8d855ce99b0 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -11,18 +11,15 @@
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
- $l-black: #7f7f7f;
- $l-red: #f00;
- $l-green: #0f0;
- $l-yellow: #ff0;
- $l-blue: #5c5cff;
- $l-magenta: #f0f;
- $l-cyan: #0ff;
- $l-white: #fff;
+ $l-black: #373b41;
+ $l-red: #c66;
+ $l-green: #b5bd68;
+ $l-yellow: #f0c674;
+ $l-blue: #81a2be;
+ $l-magenta: #b294bb;
+ $l-cyan: #8abeb7;
+ $l-white: $ci-text-color;
- .term-bold {
- font-weight: bold;
- }
.term-italic {
font-style: italic;
}
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index e9b0972bdd8..5055c318a5f 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy
- render nothing: true
+ head :ok
end
end
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 26cf74e4849..4b0ec54b3f4 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -5,6 +5,7 @@ class Admin::AppearancesController < Admin::ApplicationController
end
def preview
+ render 'preview', layout: 'devise'
end
def create
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index ec22548ddeb..23ba83aba0e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -19,6 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to admin_runners_path
end
+ def reset_health_check_token
+ @application_setting.reset_health_check_access_token!
+ flash[:notice] = 'New health check access token has been generated!'
+ redirect_to :back
+ end
+
def clear_repository_check_states
RepositoryCheck::ClearWorker.perform_async
@@ -53,6 +59,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
+ enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
+
+ params[:application_setting][:disabled_oauth_sign_in_sources] =
+ AuthHelper.button_based_providers.map(&:to_s) -
+ Array(enabled_oauth_sign_in_sources)
+
params.require(:application_setting).permit(
:default_projects_limit,
:default_branch_protection,
@@ -62,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:two_factor_grace_period,
:gravatar_enabled,
:sign_in_text,
+ :after_sign_up_text,
:help_page_text,
:home_page_url,
:after_sign_out_path,
@@ -74,6 +87,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled,
:admin_notification_email,
:user_oauth_applications,
+ :user_default_external,
:shared_runners_enabled,
:shared_runners_text,
:max_artifacts_size,
@@ -94,8 +108,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
+ :send_user_confirmation_email,
+ :container_registry_token_expire_delay,
+ :repository_storage,
+ :enabled_git_access_protocol,
restricted_visibility_levels: [],
- import_sources: []
+ import_sources: [],
+ disabled_oauth_sign_in_sources: []
)
end
end
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index fc342924987..82055006ac0 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'index' }) }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index a6db4690df0..94b5aaa71d0 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -10,6 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@members = @group.members.order("access_level DESC").page(params[:members_page])
+ @requesters = @group.requesters
@projects = @group.projects.page(params[:projects_page])
end
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
new file mode 100644
index 00000000000..241c7be0ea1
--- /dev/null
+++ b/app/controllers/admin/health_check_controller.rb
@@ -0,0 +1,5 @@
+class Admin::HealthCheckController < Admin::ApplicationController
+ def show
+ @errors = HealthCheck::Utils.process_checks('standard')
+ end
+end
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 4e85b6b4cf2..cbfc4581411 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -22,7 +22,6 @@ class Admin::HooksController < Admin::ApplicationController
redirect_to admin_hooks_path
end
-
def test
@hook = SystemHook.find(params[:hook_id])
data = {
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index cb33fdd9763..054bb52b696 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format|
format.html
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 87986fdf8b1..0d2f4f6eb38 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -5,11 +5,12 @@ class Admin::ProjectsController < Admin::ApplicationController
def index
@projects = Project.all
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
- @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
+ @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
- @projects = @projects.non_archived unless params[:with_archived].present?
+ @projects = @projects.non_archived unless params[:archived].present?
+ @projects = @projects.personal(current_user) if params[:personal].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
@@ -20,7 +21,8 @@ class Admin::ProjectsController < Admin::ApplicationController
@group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
end
- @project_members = @project.project_members.page(params[:project_members_page])
+ @project_members = @project.members.page(params[:project_members_page])
+ @requesters = @project.requesters
end
def transfer
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index d25619d94e0..bc65dcc33d3 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -1,15 +1,12 @@
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
- def index
- @runner_projects = project.runner_projects.all
- @runner_project = project.runner_projects.new
- end
-
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- if @runner.assign_to(@project, current_user)
+ runner_project = @runner.assign_to(@project, current_user)
+
+ if runner_project.persisted?
redirect_to admin_runner_path(@runner)
else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index a701d49b844..7345c91f67d 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -9,23 +9,18 @@ class Admin::RunnersController < Admin::ApplicationController
end
def show
- @builds = @runner.builds.order('id DESC').first(30)
- @projects =
- if params[:search].present?
- ::Project.search(params[:search])
- else
- Project.all
- end
- @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any?
- @projects = @projects.page(params[:page]).per(30)
+ assign_builds_and_projects
end
def update
- @runner.update_attributes(runner_params)
-
- respond_to do |format|
- format.js
- format.html { redirect_to admin_runner_path(@runner) }
+ if @runner.update_attributes(runner_params)
+ respond_to do |format|
+ format.js
+ format.html { redirect_to admin_runner_path(@runner) }
+ end
+ else
+ assign_builds_and_projects
+ render 'show'
end
end
@@ -58,6 +53,18 @@ class Admin::RunnersController < Admin::ApplicationController
end
def runner_params
- params.require(:runner).permit(:token, :description, :tag_list, :active)
+ params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
+ end
+
+ def assign_builds_and_projects
+ @builds = runner.builds.order('id DESC').first(30)
+ @projects =
+ if params[:search].present?
+ ::Project.search(params[:search])
+ else
+ Project.all
+ end
+ @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
+ @projects = @projects.page(params[:page]).per(30)
end
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 377e9741e5f..3a2f0185315 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
else
spam_log.destroy
- render nothing: true
+ head :ok
end
end
end
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
new file mode 100644
index 00000000000..e4c73008826
--- /dev/null
+++ b/app/controllers/admin/system_info_controller.rb
@@ -0,0 +1,59 @@
+class Admin::SystemInfoController < Admin::ApplicationController
+ EXCLUDED_MOUNT_OPTIONS = [
+ 'nobrowse',
+ 'read-only',
+ 'ro'
+ ]
+
+ EXCLUDED_MOUNT_TYPES = [
+ 'autofs',
+ 'binfmt_misc',
+ 'cgroup',
+ 'debugfs',
+ 'devfs',
+ 'devpts',
+ 'devtmpfs',
+ 'efivarfs',
+ 'fuse.gvfsd-fuse',
+ 'fuseblk',
+ 'fusectl',
+ 'hugetlbfs',
+ 'mqueue',
+ 'proc',
+ 'pstore',
+ 'securityfs',
+ 'sysfs',
+ 'tmpfs',
+ 'tracefs',
+ 'vfat'
+ ]
+
+ def show
+ system_info = Vmstat.snapshot
+ mounts = Sys::Filesystem.mounts
+
+ @disks = []
+ mounts.each do |mount|
+ mount_options = mount.options.split(',')
+
+ next if (EXCLUDED_MOUNT_OPTIONS & mount_options).any?
+ next if (EXCLUDED_MOUNT_TYPES & [mount.mount_type]).any?
+
+ begin
+ disk = Sys::Filesystem.stat(mount.mount_point)
+ @disks.push({
+ bytes_total: disk.bytes_total,
+ bytes_used: disk.bytes_used,
+ disk_name: mount.name,
+ mount_path: disk.path
+ })
+ rescue Sys::Filesystem::Error
+ end
+ end
+
+ @cpus = system_info.cpus.length
+
+ @mem_used = system_info.memory.active_bytes
+ @mem_total = system_info.memory.total_bytes
+ end
+end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f2f654c7bcd..f35f4a8c811 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -119,6 +119,7 @@ class Admin::UsersController < Admin::ApplicationController
user_params_with_pass.merge!(
password: params[:user][:password],
password_confirmation: params[:user][:password_confirmation],
+ password_expires_at: Time.now
)
end
@@ -153,7 +154,7 @@ class Admin::UsersController < Admin::ApplicationController
respond_to do |format|
format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 17b3f49aed1..9cc31620d9f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,8 +6,9 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include WorkhorseHelper
- before_action :authenticate_user_from_token!
+ before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
@@ -23,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
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 :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?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -35,6 +36,10 @@ class ApplicationController < ActionController::Base
render_404
end
+ rescue_from Gitlab::Access::AccessDeniedError do |exception|
+ render_403
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
@@ -63,17 +68,10 @@ class ApplicationController < ActionController::Base
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!
- user_token = if params[:authenticity_token].presence
- 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)
+ # This filter handles both private tokens and personal access tokens
+ def authenticate_user_from_private_token!
+ token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user
# Notice we are passing store false, so the user is not
@@ -176,14 +174,14 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
+ if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
redirect_to new_profile_password_path and return
end
end
def check_2fa_requirement
- if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
- redirect_to new_profile_two_factor_auth_path
+ if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
+ redirect_to profile_two_factor_auth_path
end
end
@@ -232,7 +230,7 @@ class ApplicationController < ActionController::Base
end
def configure_permitted_parameters
- devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) }
+ devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email, :password, :login, :remember_me, :otp_attempt])
end
def hexdigest(string)
@@ -263,7 +261,7 @@ class ApplicationController < ActionController::Base
# internal repos where you are not a member. Enable this filter
# or improve current implementation to filter only issues you
# created or assigned or mentioned
- #@filter_params[:authorized_only] = true
+ # @filter_params[:authorized_only] = true
end
@filter_params
@@ -325,6 +323,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
+ def gitlab_project_import_enabled?
+ current_application_settings.import_sources.include?('gitlab_project')
+ end
+
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
@@ -342,6 +344,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
+ def browser_supports_u2f?
+ browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
+ end
+
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
@@ -355,6 +361,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
+ # U2F (universal 2nd factor) devices need a unique identifier for the application
+ # to perform authentication.
+ # https://developers.yubico.com/U2F/App_ID.html
+ def u2f_app_id
+ request.base_url
+ end
+
private
def set_default_sort
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index eb0abc80ab4..c89678cf2d8 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -31,6 +31,25 @@ class AutocompleteController < ApplicationController
render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
end
+ def projects
+ project = Project.find_by_id(params[:project_id])
+
+ projects = current_user.authorized_projects
+ projects = projects.search(params[:search]) if params[:search].present?
+ projects = projects.select do |project|
+ current_user.can?(:admin_issue, project)
+ end
+
+ no_project = {
+ id: 0,
+ name_with_namespace: 'No project',
+ }
+ projects.unshift(no_project)
+ projects.delete(project)
+
+ render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
+ end
+
private
def find_users
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 8bf71a1adbb..aa894fde36b 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -25,7 +25,7 @@ module Ci
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"
+ send_file image.path, filename: image.name, disposition: 'inline', type: "image/svg+xml"
end
protected
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index d5918a7af3b..998b8adc411 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
# Returns nil
def prompt_for_two_factor(user)
session[:otp_user_id] = user.id
+ setup_u2f_authentication(user)
+ render 'devise/sessions/two_factor'
+ end
+
+ def authenticate_with_two_factor
+ user = self.resource = find_user
+
+ if user_params[:otp_attempt].present? && session[:otp_user_id]
+ authenticate_with_two_factor_via_otp(user)
+ elsif user_params[:device_response].present? && session[:otp_user_id]
+ authenticate_with_two_factor_via_u2f(user)
+ elsif user && user.valid_password?(user_params[:password])
+ prompt_for_two_factor(user)
+ end
+ end
+
+ private
+
+ def authenticate_with_two_factor_via_otp(user)
+ if valid_otp_attempt?(user)
+ # Remove any lingering user data from login
+ session.delete(:otp_user_id)
+
+ remember_me(user) if user_params[:remember_me] == '1'
+ sign_in(user)
+ else
+ flash.now[:alert] = 'Invalid two-factor code.'
+ render :two_factor
+ end
+ end
+
+ # Authenticate using the response from a U2F (universal 2nd factor) device
+ def authenticate_with_two_factor_via_u2f(user)
+ if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
+ # Remove any lingering user data from login
+ session.delete(:otp_user_id)
+ session.delete(:challenges)
+
+ sign_in(user)
+ else
+ flash.now[:alert] = 'Authentication via U2F device failed.'
+ prompt_for_two_factor(user)
+ end
+ end
+
+ # Setup in preparation of communication with a U2F (universal 2nd factor) device
+ # Actual communication is performed using a Javascript API
+ def setup_u2f_authentication(user)
+ key_handles = user.u2f_registrations.pluck(:key_handle)
+ u2f = U2F::U2F.new(u2f_app_id)
- render 'devise/sessions/two_factor' and return
+ if key_handles.present?
+ sign_requests = u2f.authentication_requests(key_handles)
+ challenges = sign_requests.map(&:challenge)
+ session[:challenges] = challenges
+ gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
+ sign_requests: sign_requests,
+ browser_supports_u2f: browser_supports_u2f? })
+ end
end
end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 787416c17ab..dacb5679dd3 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -122,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 ||= @ref
+ @mr_target_branch ||= @ref
end
end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
new file mode 100644
index 00000000000..52682ef9dc9
--- /dev/null
+++ b/app/controllers/concerns/membership_actions.rb
@@ -0,0 +1,44 @@
+module MembershipActions
+ extend ActiveSupport::Concern
+ include MembersHelper
+
+ def request_access
+ membershipable.request_access(current_user)
+
+ redirect_to polymorphic_path(membershipable),
+ notice: 'Your request for access has been queued for review.'
+ end
+
+ def approve_access_request
+ @member = membershipable.requesters.find(params[:id])
+
+ return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
+
+ @member.accept_request
+
+ redirect_to polymorphic_url([membershipable, :members])
+ end
+
+ def leave
+ @member = membershipable.members.find_by(user_id: current_user) ||
+ membershipable.requesters.find_by(user_id: current_user)
+ Members::DestroyService.new(@member, current_user).execute
+
+ source_type = @member.real_source_type.humanize(capitalize: false)
+ notice =
+ if @member.request?
+ "Your access request to the #{source_type} has been withdrawn."
+ else
+ "You left the \"#{@member.source.human_name}\" #{source_type}."
+ end
+ redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+
+ redirect_to redirect_path, notice: notice
+ end
+
+ protected
+
+ def membershipable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
new file mode 100644
index 00000000000..036777c80c1
--- /dev/null
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -0,0 +1,31 @@
+module ToggleAwardEmoji
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authenticate_user!, only: [:toggle_award_emoji]
+ end
+
+ def toggle_award_emoji
+ name = params.require(:name)
+
+ awardable.toggle_award_emoji(name, current_user)
+ TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+
+ render json: { ok: true }
+ end
+
+ private
+
+ def to_todoable(awardable)
+ case awardable
+ when Note
+ awardable.noteable
+ else
+ awardable
+ end
+ end
+
+ def awardable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 8a43c0b93c4..9e3b9be2ff4 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -6,7 +6,7 @@ module ToggleSubscriptionAction
subscribable_resource.toggle_subscription(current_user)
- render nothing: true
+ head :ok
end
private
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 7b66ad3f92c..3da44b9b888 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -1,5 +1,4 @@
class ConfirmationsController < Devise::ConfirmationsController
-
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 71ba6153021..de6bc689bb7 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,5 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.page(params[:page])
+ @group_members = current_user.group_members.includes(:source).page(params[:page])
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 71acc244a91..c08eb811532 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -28,7 +28,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
- @projects = current_user.starred_projects.sorted_by_activity
+ @projects = current_user.viewable_starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 5abf97342c3..3a2db3e6eeb 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,44 +1,39 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy, :destroy_all]
+ include TodosHelper
+
+ before_action :find_todos, only: [:index, :destroy_all]
def index
@todos = @todos.page(params[:page])
end
def destroy
- todo.done
-
- todo_notice = 'Todo was successfully marked as done.'
+ TodoService.new.mark_todos_as_done([todo], current_user)
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
+ format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.js { head :ok }
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
def destroy_all
- @todos.each(&:done)
+ TodoService.new.mark_todos_as_done(@todos, current_user)
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
+ format.js { head :ok }
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
private
def todo
- @todo ||= current_user.todos.find(params[:id])
+ @todo ||= find_todos.find(params[:id])
end
def find_todos
- @todos = TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params).execute
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 1dce4a21729..4dda4e51f6a 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -25,7 +25,7 @@ class DashboardController < Dashboard::ApplicationController
def load_events
projects =
if params[:filter] == "starred"
- current_user.starred_projects
+ current_user.viewable_starred_projects
else
current_user.authorized_projects
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index d5ef33888c6..9fc41a12536 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,6 +1,8 @@
class Groups::GroupMembersController < Groups::ApplicationController
+ include MembershipActions
+
# Authorize
- before_action :authorize_admin_group_member!, except: [:index, :leave]
+ before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@@ -13,6 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
+ @requesters = @group.requesters if can?(current_user, :admin_group, @group)
@group_member = @group.group_members.new
end
@@ -32,15 +35,14 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
- @group_member = @group.group_members.find(params[:id])
-
- return render_403 unless can?(current_user, :destroy_group_member, @group_member)
+ @group_member = @group.members.find_by(id: params[:id]) ||
+ @group.requesters.find_by(id: params[:id])
- @group_member.destroy
+ Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
@@ -58,25 +60,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
- def leave
- @group_member = @group.group_members.find_by(user_id: current_user)
-
- if can?(current_user, :destroy_group_member, @group_member)
- @group_member.destroy
-
- redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
- else
- if @group.last_owner?(current_user)
- redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
- else
- return render_403
- end
- end
- end
-
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
+
+ # MembershipActions concern
+ alias_method :membershipable, :group
end
diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb
deleted file mode 100644
index de13b16ccf2..00000000000
--- a/app/controllers/groups/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class Groups::NotificationSettingsController < Groups::ApplicationController
- before_action :authenticate_user!
-
- def update
- notification_setting = current_user.notification_settings_for(group)
- saved = notification_setting.update_attributes(notification_setting_params)
-
- render json: { saved: saved }
- end
-
- private
-
- def notification_setting_params
- params.require(:notification_setting).permit(:level)
- end
-end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index ee4fcc4e360..a04bf7df722 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -37,15 +37,12 @@ class GroupsController < Groups::ApplicationController
end
def show
- @last_push = current_user.recent_push if current_user
-
- @projects = @projects.includes(:namespace)
- @projects = @projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
+ if current_user
+ @last_push = current_user.recent_push
+ @notification_setting = current_user.notification_settings_for(group)
+ end
- @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
+ setup_projects
respond_to do |format|
format.html
@@ -97,6 +94,16 @@ class GroupsController < Groups::ApplicationController
protected
+ def setup_projects
+ @projects = @projects.includes(:namespace)
+ @projects = @projects.sorted_by_activity
+ @projects = filter_projects(@projects)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
+
+ @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
+ end
+
def authorize_create_group!
unless can?(current_user, :create_group, nil)
return render_404
diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb
new file mode 100644
index 00000000000..037da7d2bce
--- /dev/null
+++ b/app/controllers/health_check_controller.rb
@@ -0,0 +1,22 @@
+class HealthCheckController < HealthCheck::HealthCheckController
+ before_action :validate_health_check_access!
+
+ private
+
+ def validate_health_check_access!
+ render_404 unless token_valid?
+ end
+
+ def token_valid?
+ token = params[:token].presence || request.headers['TOKEN']
+ token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(
+ token,
+ current_application_settings.health_check_access_token
+ )
+ end
+
+ def render_404
+ render file: Rails.root.join('public', '404'), layout: false, status: '404'
+ end
+end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 93a7ace3530..7e8597a5eb3 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,5 +1,4 @@
class Import::BaseController < ApplicationController
-
private
def get_or_create_namespace
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 18300390851..99b10b2f9b3 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -5,7 +5,6 @@ class Import::FogbugzController < Import::BaseController
rescue_from Fogbugz::AuthenticationException, with: :fogbugz_unauthorized
def new
-
end
def callback
@@ -22,7 +21,6 @@ class Import::FogbugzController < Import::BaseController
end
def new_user_map
-
end
def create_user_map
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 67bf4190e7e..9c1b0eb20f4 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,14 +1,29 @@
class Import::GithubController < Import::BaseController
before_action :verify_github_import_enabled
- before_action :github_auth, except: :callback
+ before_action :github_auth, only: [:status, :jobs, :create]
rescue_from Octokit::Unauthorized, with: :github_unauthorized
+ helper_method :logged_in_with_github?
+
+ def new
+ if logged_in_with_github?
+ go_to_github_for_permissions
+ elsif session[:github_access_token]
+ redirect_to status_import_github_url
+ end
+ end
+
def callback
session[:github_access_token] = client.get_token(params[:code])
redirect_to status_import_github_url
end
+ def personal_access_token
+ session[:github_access_token] = params[:personal_access_token]
+ redirect_to status_import_github_url
+ end
+
def status
@repos = client.repos
@already_added_projects = current_user.created_projects.where(import_type: "github")
@@ -57,10 +72,14 @@ class Import::GithubController < Import::BaseController
end
def github_unauthorized
- go_to_github_for_permissions
+ session[:github_access_token] = nil
+ redirect_to new_import_github_url,
+ alert: 'Access denied to your GitHub account.'
end
- private
+ def logged_in_with_github?
+ current_user.identities.exists?(provider: 'github')
+ end
def access_params
{ github_access_token: session[:github_access_token] }
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
new file mode 100644
index 00000000000..30df1fb2fec
--- /dev/null
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -0,0 +1,49 @@
+class Import::GitlabProjectsController < Import::BaseController
+ before_action :verify_gitlab_project_import_enabled
+
+ def new
+ @namespace_id = project_params[:namespace_id]
+ @namespace_name = Namespace.find(project_params[:namespace_id]).name
+ @path = project_params[:path]
+ end
+
+ def create
+ unless file_is_valid?
+ return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
+ end
+
+ imported_file = project_params[:file].path + "-import"
+
+ FileUtils.copy_entry(project_params[:file].path, imported_file)
+
+ @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
+ current_user,
+ File.expand_path(imported_file),
+ project_params[:path]).execute
+
+ if @project.saved?
+ redirect_to(
+ project_path(@project),
+ notice: "Project '#{@project.name}' is being imported."
+ )
+ else
+ redirect_back_or_default(options: { alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" })
+ end
+ end
+
+ private
+
+ def file_is_valid?
+ project_params[:file] && project_params[:file].respond_to?(:read)
+ end
+
+ def verify_gitlab_project_import_enabled
+ render_404 unless gitlab_project_import_enabled?
+ end
+
+ def project_params
+ params.permit(
+ :path, :namespace_id, :file
+ )
+ end
+end
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
index eecbe380c9e..a4c4ad23027 100644
--- a/app/controllers/import/gitorious_controller.rb
+++ b/app/controllers/import/gitorious_controller.rb
@@ -44,5 +44,4 @@ class Import::GitoriousController < Import::BaseController
def verify_gitorious_import_enabled
render_404 unless gitorious_import_enabled?
end
-
end
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index e0de31f2251..8d0de158f98 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -3,7 +3,6 @@ class Import::GoogleCodeController < Import::BaseController
before_action :user_map, only: [:new_user_map, :create_user_map]
def new
-
end
def callback
@@ -34,7 +33,6 @@ class Import::GoogleCodeController < Import::BaseController
end
def new_user_map
-
end
def create_user_map
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 94bb108c5f5..58964a0e65d 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -5,7 +5,6 @@ class InvitesController < ApplicationController
respond_to :html
def show
-
end
def accept
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
new file mode 100644
index 00000000000..014b9b43ff2
--- /dev/null
+++ b/app/controllers/jwt_controller.rb
@@ -0,0 +1,49 @@
+class JwtController < ApplicationController
+ skip_before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token
+ before_action :authenticate_project_or_user
+
+ SERVICES = {
+ Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService,
+ }
+
+ def auth
+ service = SERVICES[params[:service]]
+ return head :not_found unless service
+
+ result = service.new(@project, @user, auth_params).execute
+
+ render json: result, status: result[:http_status]
+ end
+
+ private
+
+ def authenticate_project_or_user
+ authenticate_with_http_basic do |login, password|
+ # if it's possible we first try to authenticate project with login and password
+ @project = authenticate_project(login, password)
+ return if @project
+
+ @user = authenticate_user(login, password)
+ return if @user
+
+ render_403
+ end
+ end
+
+ def auth_params
+ params.permit(:service, :scope, :account, :client_id)
+ end
+
+ def authenticate_project(login, password)
+ if login == 'gitlab-ci-token'
+ Project.find_by(builds_enabled: true, runners_token: password)
+ end
+ end
+
+ def authenticate_user(login, password)
+ user = Gitlab::Auth.find_with_user_password(login, password)
+ Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
+ user
+ end
+end
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
new file mode 100644
index 00000000000..8ec4bb1233f
--- /dev/null
+++ b/app/controllers/notification_settings_controller.rb
@@ -0,0 +1,50 @@
+class NotificationSettingsController < ApplicationController
+ before_action :authenticate_user!
+
+ def create
+ return render_404 unless can_read?(resource)
+
+ @notification_setting = current_user.notification_settings_for(resource)
+ @saved = @notification_setting.update_attributes(notification_setting_params)
+
+ render_response
+ end
+
+ def update
+ @notification_setting = current_user.notification_settings.find(params[:id])
+ @saved = @notification_setting.update_attributes(notification_setting_params)
+
+ render_response
+ end
+
+ private
+
+ def resource
+ @resource ||=
+ if params[:project_id].present?
+ Project.find(params[:project_id])
+ elsif params[:namespace_id].present?
+ Group.find(params[:namespace_id])
+ end
+ end
+
+ def can_read?(resource)
+ ability_name = resource.class.name.downcase
+ ability_name = "read_#{ability_name}".to_sym
+
+ can?(current_user, ability_name, resource)
+ end
+
+ def render_response
+ render json: {
+ html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
+ saved: @saved
+ }
+ end
+
+ def notification_setting_params
+ allowed_fields = NotificationSetting::EMAIL_EVENTS.dup
+ allowed_fields << :level
+ params.require(:notification_setting).permit(allowed_fields)
+ end
+end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index c6bdd0602c1..0f54dfa4efc 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
def verify_user_oauth_applications_enabled
return if current_application_settings.user_oauth_applications?
- redirect_to applications_profile_url
+ redirect_to profile_path
end
def set_index_vars
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index df98f56a1cd..f35d631df0c 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -97,7 +97,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_signup_error
end
- def handle_service_ticket provider, ticket
+ def handle_service_ticket(provider, ticket)
Gitlab::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 175afbf8425..69959fe3687 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink
provider = params[:provider]
- current_user.identities.find_by(provider: provider).destroy
+ current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
redirect_to profile_account_path
end
end
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 0ede9b8e21b..1c24c4db993 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
respond_to do |format|
format.html { redirect_to profile_emails_url }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index a12549d6bcb..830e0b9591b 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -32,7 +32,7 @@ class Profiles::KeysController < Profiles::ApplicationController
respond_to do |format|
format.html { redirect_to profile_keys_url }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 18ee55c839a..b8b71d295f6 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,8 +1,9 @@
class Profiles::NotificationsController < Profiles::ApplicationController
def show
- @user = current_user
- @group_notifications = current_user.notification_settings.for_groups
- @project_notifications = current_user.notification_settings.for_projects
+ @user = current_user
+ @group_notifications = current_user.notification_settings.for_groups.order(:id)
+ @project_notifications = current_user.notification_settings.for_projects.order(:id)
+ @global_notification_setting = current_user.global_notification_setting
end
def update
@@ -16,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :notification_level)
+ params.require(:user).permit(:notification_email)
end
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..508b82a9a6c
--- /dev/null
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -0,0 +1,42 @@
+class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
+ before_action :load_personal_access_tokens, only: :index
+
+ def index
+ @personal_access_token = current_user.personal_access_tokens.build
+ end
+
+ def create
+ @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+
+ if @personal_access_token.save
+ flash[:personal_access_token] = @personal_access_token.token
+ redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ else
+ load_personal_access_tokens
+ render :index
+ end
+ end
+
+ def revoke
+ @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+
+ if @personal_access_token.revoke!
+ flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ else
+ flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ end
+
+ redirect_to profile_personal_access_tokens_path
+ end
+
+ private
+
+ def personal_access_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at)
+ end
+
+ def load_personal_access_tokens
+ @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
+ @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
+ end
+end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 8f83fdd02bc..6a358fdcc05 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -1,7 +1,7 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
- def new
+ def show
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
end
@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
- if two_factor_authentication_required?
+ if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired?
- flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
+ 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)}."
+ flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end
end
@qr_code = build_qr_code
+ setup_u2f_registration
end
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
- current_user.two_factor_enabled = true
+ current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else
@error = 'Invalid pin code'
@qr_code = build_qr_code
+ setup_u2f_registration
+ render 'show'
+ end
+ end
+
+ # A U2F (universal 2nd factor) device's information is stored after successful
+ # registration, which is then used while 2FA authentication is taking place.
+ def create_u2f
+ @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
- render 'new'
+ if @u2f_registration.persisted?
+ session.delete(:challenges)
+ redirect_to profile_account_path, notice: "Your U2F device was registered!"
+ else
+ @qr_code = build_qr_code
+ setup_u2f_registration
+ render :show
end
end
@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host
Gitlab.config.gitlab.host
end
+
+ # Setup in preparation of communication with a U2F (universal 2nd factor) device
+ # Actual communication is performed using a Javascript API
+ def setup_u2f_registration
+ @u2f_registration ||= U2fRegistration.new
+ @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
+ u2f = U2F::U2F.new(u2f_app_id)
+
+ registration_requests = u2f.registration_requests
+ sign_requests = u2f.authentication_requests(@registration_key_handles)
+ session[:challenges] = registration_requests.map(&:challenge)
+
+ gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
+ register_requests: registration_requests,
+ sign_requests: sign_requests,
+ browser_supports_u2f: browser_supports_u2f? })
+ end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index be872a93fee..996909a28c6 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -26,7 +26,7 @@ class Projects::ApplicationController < ApplicationController
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
- if @project && can?(current_user, :read_project, @project)
+ if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
end
@@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController
end
def require_branch_head
- unless @repository.branch_names.include?(@ref)
+ unless @repository.branch_exists?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on a branch"
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index cfea1266516..f11c8321464 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,22 +1,18 @@
class Projects::ArtifactsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_build!
+ before_action :authorize_update_build!, only: [:keep]
+ before_action :validate_artifacts!
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)
@@ -34,10 +30,19 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
+ def keep
+ build.keep_artifacts!
+ redirect_to namespace_project_build_path(project.namespace, project, build)
+ end
+
private
+ def validate_artifacts!
+ render_404 unless build.artifacts?
+ end
+
def build
- @build ||= project.builds.unscoped.find_by!(id: params[:build_id])
+ @build ||= project.builds.find_by!(id: params[:build_id])
end
def artifacts_file
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 72921b3aa14..5962f74c39b 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController
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
+ send_git_blob @repository, @blob
else
render_404
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index cd8b2911674..5356fdf010d 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -16,6 +16,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
+ before_action :validate_diff_params, only: :diff
def new
commit unless @repository.empty?
@@ -56,7 +57,7 @@ class Projects::BlobController < Projects::ApplicationController
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
- @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight
+ @diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight
render layout: false
end
@@ -146,4 +147,10 @@ class Projects::BlobController < Projects::ApplicationController
file_content_encoding: params[:encoding]
}
end
+
+ def validate_diff_params
+ if [:since, :to, :offset].any? { |key| params[key].blank? }
+ render nothing: true
+ end
+ end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index d09e7375b67..dd9508da049 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController
redirect_to namespace_project_branches_path(@project.namespace,
@project), status: 303
end
- format.js { render status: status[:return_code] }
+ format.js { render nothing: true, status: status[:return_code] }
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index b8b9e78427d..ef3051d7519 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -26,9 +26,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def show
- @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id)
- @commit = @build.commit
+ @pipeline = @build.pipeline
respond_to do |format|
format.html
@@ -38,12 +38,20 @@ class Projects::BuildsController < Projects::ApplicationController
end
end
+ def trace
+ respond_to do |format|
+ format.json do
+ render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
+ end
+ end
+ end
+
def retry
unless @build.retryable?
return render_404
end
- build = Ci::Build.retry(@build)
+ build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
@@ -73,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
- @build ||= project.builds.unscoped.find_by!(id: params[:id])
+ @build ||= project.builds.find_by!(id: params[:id])
end
def build_path(build)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index a202cb38692..37d6521026c 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -17,12 +17,19 @@ class Projects::CommitController < Projects::ApplicationController
def show
apply_diff_view_cookie!
- @line_notes = commit.notes.inline
+ @grouped_diff_notes = commit.notes.grouped_diff_notes
+ @notes = commit.notes.non_diff_notes.fresh
+
+ Banzai::NoteRenderer.render(
+ @grouped_diff_notes.values.flatten + @notes,
+ @project,
+ current_user,
+ )
+
@note = @project.build_commit_note(commit)
- @notes = commit.notes.not_inline.fresh
+
@noteable = @commit
- @comments_allowed = @reply_allowed = true
- @comments_target = {
+ @comments_target = {
noteable_type: 'Commit',
commit_id: @commit.id
}
@@ -46,7 +53,7 @@ class Projects::CommitController < Projects::ApplicationController
def retry_builds
ci_builds.latest.failed.each do |build|
if build.retryable?
- Ci::Build.retry(build)
+ Ci::Build.retry(build, current_user)
end
end
@@ -67,10 +74,10 @@ class Projects::CommitController < Projects::ApplicationController
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
success_path: successful_change_path, failure_path: failed_change_path)
end
-
+
def cherry_pick
assign_change_commit_vars(@commit.cherry_pick_branch_name)
-
+
return render_404 if @target_branch.blank?
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
@@ -99,12 +106,12 @@ class Projects::CommitController < Projects::ApplicationController
@commit ||= @project.commit(params[:id])
end
- def ci_commits
- @ci_commits ||= project.ci_commits.where(sha: commit.sha)
+ def pipelines
+ @pipelines ||= project.pipelines.where(sha: commit.sha)
end
def ci_builds
- @ci_builds ||= Ci::Build.where(commit: ci_commits)
+ @ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
def define_show_vars
@@ -114,11 +121,10 @@ class Projects::CommitController < Projects::ApplicationController
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 = CommitStatus.where(commit: ci_commits)
- @builds = Ci::Build.where(commit: ci_commits)
+ @statuses = CommitStatus.where(pipeline: pipelines)
+ @builds = Ci::Build.where(pipeline: pipelines)
end
def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 671d5c23024..d240b9fe989 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -14,15 +14,24 @@ class Projects::CompareController < Projects::ApplicationController
def show
compare = CompareService.new.
- execute(@project, @head_ref, @project, @base_ref, diff_options)
+ execute(@project, @head_ref, @project, @start_ref, diff_options)
if compare
@commits = Commit.decorate(compare.commits, @project)
+
+ @start_commit = @project.commit(@start_ref)
@commit = @project.commit(@head_ref)
- @base_commit = @project.merge_base_commit(@base_ref, @head_ref)
+ @base_commit = @project.merge_base_commit(@start_ref, @head_ref)
+
@diffs = compare.diffs(diff_options)
- @diff_refs = [@base_commit, @commit]
- @line_notes = []
+ @diff_refs = Gitlab::Diff::DiffRefs.new(
+ base_sha: @base_commit.try(:sha),
+ start_sha: @start_commit.try(:sha),
+ head_sha: @commit.try(:sha)
+ )
+
+ @diff_notes_disabled = true
+ @grouped_diff_notes = {}
end
end
@@ -34,12 +43,12 @@ class Projects::CompareController < Projects::ApplicationController
private
def assign_ref_vars
- @base_ref = Addressable::URI.unescape(params[:from])
+ @start_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)
+ find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end
end
diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb
new file mode 100644
index 00000000000..d1f46497207
--- /dev/null
+++ b/app/controllers/projects/container_registry_controller.rb
@@ -0,0 +1,34 @@
+class Projects::ContainerRegistryController < Projects::ApplicationController
+ before_action :verify_registry_enabled
+ before_action :authorize_read_container_image!
+ before_action :authorize_update_container_image!, only: [:destroy]
+ layout 'project'
+
+ def index
+ @tags = container_registry_repository.tags
+ end
+
+ def destroy
+ url = namespace_project_container_registry_index_path(project.namespace, project)
+
+ if tag.delete
+ redirect_to url
+ else
+ redirect_to url, alert: 'Failed to remove tag'
+ end
+ end
+
+ private
+
+ def verify_registry_enabled
+ render_404 unless Gitlab.config.registry.enabled
+ end
+
+ def container_registry_repository
+ @container_registry_repository ||= project.container_registry_repository
+ end
+
+ def tag
+ @tag ||= container_registry_repository.tag(params[:id])
+ end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
new file mode 100644
index 00000000000..4b433796161
--- /dev/null
+++ b/app/controllers/projects/environments_controller.rb
@@ -0,0 +1,49 @@
+class Projects::EnvironmentsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_environment!
+ before_action :authorize_create_environment!, only: [:new, :create]
+ before_action :authorize_update_environment!, only: [:destroy]
+ before_action :environment, only: [:show, :destroy]
+
+ def index
+ @environments = project.environments
+ end
+
+ def show
+ @deployments = environment.deployments.order(id: :desc).page(params[:page])
+ end
+
+ def new
+ @environment = project.environments.new
+ end
+
+ def create
+ @environment = project.environments.create(create_params)
+
+ if @environment.persisted?
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ if @environment.destroy
+ flash[:notice] = 'Environment was successfully removed.'
+ else
+ flash[:alert] = 'Failed to remove environment.'
+ end
+
+ redirect_to namespace_project_environments_path(project.namespace, project)
+ end
+
+ private
+
+ def create_params
+ params.require(:environment).permit(:name)
+ end
+
+ def environment
+ @environment ||= project.environments.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
index 54a0c447aee..cf53ad0a670 100644
--- a/app/controllers/projects/find_file_controller.rb
+++ b/app/controllers/projects/find_file_controller.rb
@@ -1,26 +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
+# 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/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
new file mode 100644
index 00000000000..40a8b7940d9
--- /dev/null
+++ b/app/controllers/projects/git_http_controller.rb
@@ -0,0 +1,200 @@
+# This file should be identical in GitLab Community Edition and Enterprise Edition
+
+class Projects::GitHttpController < Projects::ApplicationController
+ include ActionController::HttpAuthentication::Basic
+ include KerberosSpnegoHelper
+
+ attr_reader :user
+
+ # Git clients will not know what authenticity token to send along
+ skip_before_action :verify_authenticity_token
+ skip_before_action :repository
+ before_action :authenticate_user
+ before_action :ensure_project_found!
+
+ # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
+ # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
+ def info_refs
+ if upload_pack? && upload_pack_allowed?
+ render_ok
+ elsif receive_pack? && receive_pack_allowed?
+ render_ok
+ elsif http_blocked?
+ render_not_allowed
+ else
+ render_not_found
+ end
+ end
+
+ # POST /foo/bar.git/git-upload-pack (git pull)
+ def git_upload_pack
+ if upload_pack? && upload_pack_allowed?
+ render_ok
+ else
+ render_not_found
+ end
+ end
+
+ # POST /foo/bar.git/git-receive-pack" (git push)
+ def git_receive_pack
+ if receive_pack? && receive_pack_allowed?
+ render_ok
+ else
+ render_not_found
+ end
+ end
+
+ private
+
+ def authenticate_user
+ if project && project.public? && upload_pack?
+ return # Allow access
+ end
+
+ if allow_basic_auth? && basic_auth_provided?
+ login, password = user_name_and_password(request)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+
+ if auth_result.type == :ci && upload_pack?
+ @ci = true
+ elsif auth_result.type == :oauth && !upload_pack?
+ # Not allowed
+ else
+ @user = auth_result.user
+ end
+
+ if ci? || user
+ return # Allow access
+ end
+ elsif allow_kerberos_spnego_auth? && spnego_provided?
+ @user = find_kerberos_user
+
+ if user
+ send_final_spnego_response
+ return # Allow access
+ end
+ end
+
+ send_challenges
+ render plain: "HTTP Basic: Access denied\n", status: 401
+ end
+
+ def basic_auth_provided?
+ has_basic_credentials?(request)
+ end
+
+ def send_challenges
+ challenges = []
+ challenges << 'Basic realm="GitLab"' if allow_basic_auth?
+ challenges << spnego_challenge if allow_kerberos_spnego_auth?
+ headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
+ end
+
+ def ensure_project_found!
+ render_not_found if project.blank?
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ project_id, _ = project_id_with_suffix
+ if project_id.blank?
+ @project = nil
+ else
+ @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
+ end
+ end
+
+ # This method returns two values so that we can parse
+ # params[:project_id] (untrusted input!) in exactly one place.
+ def project_id_with_suffix
+ id = params[:project_id] || ''
+
+ %w[.wiki.git .git].each do |suffix|
+ if id.end_with?(suffix)
+ # Be careful to only remove the suffix from the end of 'id'.
+ # Accidentally removing it from the middle is how security
+ # vulnerabilities happen!
+ return [id.slice(0, id.length - suffix.length), suffix]
+ end
+ end
+
+ # Something is wrong with params[:project_id]; do not pass it on.
+ [nil, nil]
+ end
+
+ def upload_pack?
+ git_command == 'git-upload-pack'
+ end
+
+ def receive_pack?
+ git_command == 'git-receive-pack'
+ end
+
+ def git_command
+ if action_name == 'info_refs'
+ params[:service]
+ else
+ action_name.dasherize
+ end
+ end
+
+ def render_ok
+ render json: Gitlab::Workhorse.git_http_ok(repository, user)
+ end
+
+ def repository
+ _, suffix = project_id_with_suffix
+ if suffix == '.wiki.git'
+ project.wiki.repository
+ else
+ project.repository
+ end
+ end
+
+ def render_not_found
+ render plain: 'Not Found', status: :not_found
+ end
+
+ def render_not_allowed
+ render plain: download_access.message, status: :forbidden
+ end
+
+ def ci?
+ @ci.present?
+ end
+
+ def upload_pack_allowed?
+ return false unless Gitlab.config.gitlab_shell.upload_pack
+
+ if user
+ download_access.allowed?
+ else
+ ci? || project.public?
+ end
+ end
+
+ def access
+ return @access if defined?(@access)
+
+ @access = Gitlab::GitAccess.new(user, project, 'http')
+ end
+
+ def download_access
+ return @download_access if defined?(@download_access)
+
+ @download_access = access.check('git-upload-pack')
+ end
+
+ def http_blocked?
+ !access.protocol_allowed?
+ end
+
+ def receive_pack_allowed?
+ return false unless Gitlab.config.gitlab_shell.receive_pack
+
+ # Skip user authorization on upload request.
+ # It will be done by the pre-receive hook in the repository.
+ user.present?
+ end
+end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index dfa9bd259e8..a60027ff477 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -27,8 +27,10 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
- if status
- flash[:notice] = 'Hook successfully executed.'
+ if status && status >= 200 && status < 400
+ flash[:notice] = "Hook executed successfully: HTTP #{status}"
+ elsif status
+ flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
@@ -61,7 +63,8 @@ class Projects::HooksController < Projects::ApplicationController
:push_events,
:tag_push_events,
:token,
- :url
+ :url,
+ :wiki_page_events
)
end
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 7756f0f0ed3..a1b84afcd91 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -20,6 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController
@project.import_retry
else
@project.import_start
+ @project.add_import_job
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 016f5dd0005..b6e80762e3c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,6 +1,7 @@
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
+ include ToggleAwardEmoji
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -61,8 +62,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
+ raw_notes = @issue.notes_with_associations.fresh
+
+ @notes = Banzai::NoteRenderer.
+ render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
+
@note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
respond_to do |format|
@@ -71,7 +76,6 @@ class Projects::IssuesController < Projects::ApplicationController
render json: @issue.to_json(include: [:milestone, :labels])
end
end
-
end
def create
@@ -110,6 +114,7 @@ class Projects::IssuesController < Projects::ApplicationController
render :edit
end
end
+
format.json do
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
end
@@ -155,7 +160,12 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
- redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
+
+ respond_to do |format|
+ format.json do
+ render json: { notice: "#{result[:count]} issues updated" }
+ end
+ end
end
protected
@@ -169,6 +179,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
+ alias_method :awardable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -214,7 +225,10 @@ class Projects::IssuesController < Projects::ApplicationController
:issues_ids,
:assignee_id,
:milestone_id,
- :state_event
+ :state_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
)
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index ff771ea6d9c..0ca675623e5 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -5,13 +5,14 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [
- :new, :create, :edit, :update, :generate, :destroy
+ :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
]
respond_to :js, :html
def index
- @labels = @project.labels.page(params[:page])
+ @labels = @project.labels.unprioritized.page(params[:page])
+ @prioritized_labels = @project.labels.prioritized
respond_to do |format|
format.html
@@ -71,6 +72,30 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ def remove_priority
+ respond_to do |format|
+ if label.update_attribute(:priority, nil)
+ format.json { render json: label }
+ else
+ message = label.errors.full_messages.uniq.join('. ')
+ format.json { render json: { message: message }, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ def set_priorities
+ Label.transaction do
+ params[:label_ids].each_with_index do |label_id, index|
+ label = @project.labels.find_by_id(label_id)
+ label.update_attribute(:priority, index) if label
+ end
+ end
+
+ respond_to do |format|
+ format.json { render json: { message: 'success' } }
+ end
+ end
+
protected
def module_enabled
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9c147b3689e..8fda5618818 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
include IssuableActions
+ include ToggleAwardEmoji
before_action :module_enabled
before_action :merge_request, only: [
@@ -52,33 +53,51 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def show
- @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)).
- group(:commit_id).count
-
respond_to do |format|
format.html
- format.json { render json: @merge_request }
- format.diff { render text: @merge_request.to_diff }
- format.patch { render text: @merge_request.to_patch }
+
+ format.json do
+ render json: @merge_request
+ end
+
+ format.patch do
+ return render_404 unless @merge_request.diff_refs
+
+ send_git_patch @project.repository, @merge_request.diff_refs
+ end
+
+ format.diff do
+ return render_404 unless @merge_request.diff_refs
+
+ send_git_diff @project.repository, @merge_request.diff_refs
+ end
end
end
def diffs
apply_diff_view_cookie!
- @commit = @merge_request.last_commit
- @base_commit = @merge_request.diff_base_commit
+ @merge_request_diff = @merge_request.merge_request_diff
- # 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
+ @commit = @merge_request.diff_head_commit
+ @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
- @comments_allowed = @reply_allowed = true
@comments_target = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
- @line_notes = @merge_request.notes.where("line_code is not null")
+
+ @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
+ @grouped_diff_notes = @merge_request.notes.grouped_diff_notes
+
+ Banzai::NoteRenderer.render(
+ @grouped_diff_notes.values.flatten,
+ @project,
+ current_user,
+ @path,
+ @project_wiki,
+ @ref
+ )
respond_to do |format|
format.html
@@ -89,7 +108,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def commits
respond_to do |format|
format.html { render 'show' }
- format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } }
+ format.json do
+ # Get commits from repository
+ # or from cache if already merged
+ @commits = @merge_request.commits
+ @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ group(:commit_id).count
+
+ render json: { html: view_to_html_string('projects/merge_requests/show/_commits') }
+ end
end
end
@@ -114,12 +141,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
- @commit = @merge_request.last_commit
+ @commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
+ @diff_notes_disabled = true
- @ci_commit = @merge_request.ci_commit
- @statuses = @ci_commit.statuses if @ci_commit
+ @pipeline = @merge_request.pipeline
+ @statuses = @pipeline.statuses if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@@ -184,19 +212,40 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
- unless @merge_request.mergeable?
+ # Disable the CI check if merge_when_build_succeeds is enabled since we have
+ # to wait until CI completes to know
+ unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?)
@status = :failed
return
end
+ if params[:sha] != @merge_request.diff_head_sha
+ @status = :sha_mismatch
+ 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?
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
- .execute(@merge_request)
- @status = :merge_when_build_succeeds
+ if params[:merge_when_build_succeeds].present?
+ unless @merge_request.pipeline
+ @status = :failed
+ return
+ end
+
+ if @merge_request.pipeline.active?
+ MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
+ .execute(@merge_request)
+ @status = :merge_when_build_succeeds
+ elsif @merge_request.pipeline.success?
+ # This can be triggered when a user clicks the auto merge button while
+ # the tests finish at about the same time
+ MergeWorker.perform_async(@merge_request.id, current_user.id, params)
+ @status = :success
+ else
+ @status = :failed
+ end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
@@ -204,7 +253,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def branch_from
- #This is always source
+ # This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
@commit = @repository.commit(params[:ref]) if params[:ref].present?
render layout: false
@@ -224,24 +273,24 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
- ci_commit = @merge_request.ci_commit
- if ci_commit
- status = ci_commit.status
- coverage = ci_commit.try(:coverage)
+ pipeline = @merge_request.pipeline
+ if pipeline
+ status = pipeline.status
+ coverage = pipeline.try(:coverage)
+
+ status ||= "preparing"
else
ci_service = @merge_request.source_project.ci_service
- status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service
+ status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
+ coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
end
end
- status = "preparing" if status.nil?
-
response = {
title: merge_request.title,
- sha: merge_request.last_commit_short_sha,
+ sha: merge_request.diff_head_commit.short_id,
status: status,
coverage: coverage
}
@@ -264,6 +313,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
+ alias_method :awardable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
@@ -297,30 +347,46 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_show_vars
- # Build a note object for comment form
- @note = @project.notes.new(noteable: @merge_request)
- @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
- @discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request
+ @commits_count = @merge_request.commits.count
- # Get commits from repository
- # or from cache if already merged
- @commits = @merge_request.commits
-
- @merge_request_diff = @merge_request.merge_request_diff
-
- @ci_commit = @merge_request.ci_commit
- @statuses = @ci_commit.statuses if @ci_commit
+ @pipeline = @merge_request.pipeline
+ @statuses = @pipeline.statuses if @pipeline
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
+
+ if request.format == :html || action_name == 'show'
+ define_show_html_vars
+ end
+ end
+
+ # Discussion tab data is only required on html requests
+ def define_show_html_vars
+ # Build a note object for comment form
+ @note = @project.notes.new(noteable: @noteable)
+
+ @discussions = @noteable.mr_and_commit_notes.
+ inc_author_project_award_emoji.
+ fresh.
+ discussions
+
+ # This is not executed lazily
+ @notes = Banzai::NoteRenderer.render(
+ @discussions.flatten,
+ @project,
+ current_user,
+ @path,
+ @project_wiki,
+ @ref
+ )
end
def define_widget_vars
- @ci_commit = @merge_request.ci_commit
- @ci_commits = [@ci_commit].compact
+ @pipeline = @merge_request.pipeline
+ @pipelines = [@pipeline].compact
closes_issues
end
@@ -333,7 +399,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
- :state_event, :description, :task_num, label_ids: []
+ :state_event, :description, :task_num, :force_remove_source_branch,
+ label_ids: []
)
end
@@ -346,4 +413,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ensure_ref_fetched
@merge_request.ensure_ref_fetched
end
+
+ def merge_when_build_succeeds_active?
+ params[:merge_when_build_succeeds].present? &&
+ @merge_request.pipeline && @merge_request.pipeline.active?
+ end
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index f7b6d137bde..da2892bfb3f 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -75,7 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format|
format.html { redirect_to namespace_project_milestones_path }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index b181c47baec..34318391dd9 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -7,7 +7,6 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :authorize_download_code!
def show
-
@url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))
@commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 707a0d0e5c6..3eacdbbd067 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,9 +1,11 @@
class Projects::NotesController < Projects::ApplicationController
+ include ToggleAwardEmoji
+
# Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
- before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
+ before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
@@ -22,6 +24,10 @@ class Projects::NotesController < Projects::ApplicationController
def create
@note = Notes::CreateService.new(project, current_user, note_params).execute
+ if @note.is_a?(Note)
+ Banzai::NoteRenderer.render([@note], @project, current_user)
+ end
+
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
@@ -31,6 +37,10 @@ class Projects::NotesController < Projects::ApplicationController
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
+ if @note.is_a?(Note)
+ Banzai::NoteRenderer.render([@note], @project, current_user)
+ end
+
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
@@ -43,7 +53,7 @@ class Projects::NotesController < Projects::ApplicationController
end
respond_to do |format|
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
@@ -52,32 +62,8 @@ class Projects::NotesController < Projects::ApplicationController
note.update_attribute(:attachment, nil)
respond_to do |format|
- format.js { render nothing: true }
- end
- end
-
- def award_toggle
- noteable = if note_params[:noteable_type] == "issue"
- project.issues.find(note_params[:noteable_id])
- else
- project.merge_requests.find(note_params[:noteable_id])
- end
-
- data = {
- author: current_user,
- is_award: true,
- note: note_params[:note].delete(":")
- }
-
- note = noteable.notes.find_by(data)
-
- if note
- note.destroy
- else
- Notes::CreateService.new(project, current_user, note_params).execute
+ format.js { head :ok }
end
-
- render json: { ok: true }
end
private
@@ -85,6 +71,7 @@ class Projects::NotesController < Projects::ApplicationController
def note
@note ||= @project.notes.find(params[:id])
end
+ alias_method :awardable, :note
def note_to_html(note)
render_to_string(
@@ -96,7 +83,7 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_to_discussion_html(note)
- return unless note.for_diff_line?
+ return unless note.diff_note?
if params[:view] == 'parallel'
template = "projects/notes/_diff_notes_with_reply_parallel"
@@ -120,7 +107,7 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_to_discussion_with_diff_html(note)
- return unless note.for_diff_line?
+ return unless note.diff_note?
render_to_string(
"projects/notes/_discussion",
@@ -131,21 +118,47 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_json(note)
- if note.valid?
+ if note.is_a?(AwardEmoji)
{
+ valid: note.valid?,
+ award: true,
+ id: note.id,
+ name: note.name
+ }
+ elsif note.valid?
+ Banzai::NoteRenderer.render([note], @project, current_user)
+
+ attrs = {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
- award: note.is_award,
+ award: false,
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
+
+ # The discussion_id is used to add the comment to the correct discussion
+ # element on the merge request page. Among other things, the discussion_id
+ # contains the sha of head commit of the merge request.
+ # When new commits are pushed into the merge request after the initial
+ # load of the merge request page, the discussion elements will still have
+ # the old discussion_ids, with the old head commit sha. The new comment,
+ # however, will have the new discussion_id with the new commit sha.
+ # To ensure that these new comments will still end up in the correct
+ # discussion element, we also send the original discussion_id, with the
+ # old commit sha, along, and fall back on this value when no discussion
+ # element with the new discussion_id could be found.
+ if note.new_diff_note? && note.position != note.original_position
+ attrs[:original_discussion_id] = note.original_discussion_id
+ end
+
+ attrs
else
{
valid: false,
- award: note.is_award,
+ award: false,
errors: note.errors
}
end
@@ -158,7 +171,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
- :attachment, :line_code, :commit_id
+ :attachment, :line_code, :commit_id, :type, :position
)
end
diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb
deleted file mode 100644
index 7d81cc03c73..00000000000
--- a/app/controllers/projects/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class Projects::NotificationSettingsController < Projects::ApplicationController
- before_action :authenticate_user!
-
- def update
- notification_setting = current_user.notification_settings_for(project)
- saved = notification_setting.update_attributes(notification_setting_params)
-
- render json: { saved: saved }
- end
-
- private
-
- def notification_setting_params
- params.require(:notification_setting).permit(:level)
- end
-end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
new file mode 100644
index 00000000000..487963fdcd7
--- /dev/null
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -0,0 +1,59 @@
+class Projects::PipelinesController < Projects::ApplicationController
+ before_action :pipeline, except: [:index, :new, :create]
+ before_action :commit, only: [:show]
+ before_action :authorize_read_pipeline!
+ before_action :authorize_create_pipeline!, only: [:new, :create]
+ before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+
+ def index
+ @scope = params[:scope]
+ all_pipelines = project.pipelines
+ @pipelines_count = all_pipelines.count
+ @running_or_pending_count = all_pipelines.running_or_pending.count
+ @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
+ @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
+ end
+
+ def new
+ @pipeline = project.pipelines.new(ref: @project.default_branch)
+ end
+
+ def create
+ @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
+ unless @pipeline.persisted?
+ render 'new'
+ return
+ end
+
+ redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
+ end
+
+ def show
+ end
+
+ def retry
+ pipeline.retry_failed(current_user)
+
+ redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ def cancel
+ pipeline.cancel_running
+
+ redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ private
+
+ def create_params
+ params.require(:pipeline).permit(:ref)
+ end
+
+ def pipeline
+ @pipeline ||= project.pipelines.find_by!(id: params[:id])
+ end
+
+ def commit
+ @commit ||= @pipeline.commit
+ end
+end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 33b2625c0ac..3435a118964 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,6 +1,8 @@
class Projects::ProjectMembersController < Projects::ApplicationController
+ include MembershipActions
+
# Authorize
- before_action :authorize_admin_project_member!, except: [:leave, :index]
+ before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
@@ -14,6 +16,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
+
if @group
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
@@ -26,6 +29,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.order('access_level DESC')
end
+ @requesters = @project.requesters if can?(current_user, :admin_project, @project)
+
@project_member = @project.project_members.new
@project_group_links = @project.project_group_links
end
@@ -45,17 +50,16 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
- @project_member = @project.project_members.find(params[:id])
+ @project_member = @project.members.find_by(id: params[:id]) ||
+ @project.requesters.find_by(id: params[:id])
- return render_403 unless can?(current_user, :destroy_project_member, @project_member)
-
- @project_member.destroy
+ Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format|
format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
@@ -73,26 +77,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
- def leave
- @project_member = @project.project_members.find_by(user_id: current_user)
-
- if can?(current_user, :destroy_project_member, @project_member)
- @project_member.destroy
-
- respond_to do |format|
- format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
- format.js { render nothing: true }
- end
- else
- if current_user == @project.owner
- message = 'You can not leave your own project. Transfer or delete the project.'
- redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
- else
- render_403
- end
- end
- end
-
def apply_import
source_project = Project.find(params[:source_project_id])
@@ -112,4 +96,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
+
+ # MembershipActions concern
+ alias_method :membershipable, :project
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index e49259c34b6..80dad758afa 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -2,12 +2,14 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
+ before_action :load_protected_branch, only: [:show, :update, :destroy]
layout "project_settings"
def index
- @branches = @project.protected_branches.to_a
+ @protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
+ gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } })
end
def create
@@ -16,35 +18,37 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
@project)
end
- def update
- protected_branch = @project.protected_branches.find(params[:id])
-
- if protected_branch &&
- protected_branch.update_attributes(
- developers_can_push: params[:developers_can_push]
- )
+ def show
+ @matching_branches = @protected_branch.matching(@project.repository.branches)
+ end
+ def update
+ if @protected_branch && @protected_branch.update_attributes(protected_branch_params)
respond_to do |format|
- format.json { render json: protected_branch, status: :ok }
+ format.json { render json: @protected_branch, status: :ok }
end
else
respond_to do |format|
- format.json { render json: protected_branch.errors, status: :unprocessable_entity }
+ format.json { render json: @protected_branch.errors, status: :unprocessable_entity }
end
end
end
def destroy
- @project.protected_branches.find(params[:id]).destroy
+ @protected_branch.destroy
respond_to do |format|
format.html { redirect_to namespace_project_protected_branches_path }
- format.js { render nothing: true }
+ format.js { head :ok }
end
end
private
+ def load_protected_branch
+ @protected_branch = @project.protected_branches.find(params[:id])
+ end
+
def protected_branch_params
params.require(:protected_branch).permit(:name, :developers_can_push)
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 10de0e60530..10d24da16d7 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController
if @blob.lfs_pointer?
send_lfs_object
else
- 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
+ send_git_blob @repository, @blob
end
else
render_404
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index bb7a6b6a5ab..d5af0341d18 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
- head :ok
+ send_git_archive @repository, ref: params[:ref], format: params[:format]
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 bedeb4a295c..8267b14941d 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -6,11 +6,12 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- return head(403) unless current_user.ci_authorized_runners.include?(@runner)
+ return head(403) unless can?(current_user, :assign_runner, @runner)
path = runners_path(project)
+ runner_project = @runner.assign_to(project, current_user)
- if @runner.assign_to(project, current_user)
+ if runner_project.persisted?
redirect_to path
else
redirect_to path, alert: 'Failed adding runner to project'
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 0dd2d6a99be..53c36635efe 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @runners = project.runners.ordered
- @specific_runners = current_user.ci_authorized_runners.
- where.not(id: project.runners).
- ordered.page(params[:page]).per(20)
+ @project_runners = project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
@@ -20,7 +19,7 @@ class Projects::RunnersController < Projects::ApplicationController
if @runner.update_attributes(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
- redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ render 'edit'
end
end
@@ -64,6 +63,6 @@ class Projects::RunnersController < Projects::ApplicationController
end
def runner_params
- params.require(:runner).permit(:description, :tag_list, :active)
+ params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 6d2901a24a4..6d0a7ee1031 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -54,7 +54,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @snippet)
- @notes = @snippet.notes.fresh
+ @notes = Banzai::NoteRenderer.render(@snippet.notes.fresh, @project, current_user)
@noteable = @snippet
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 46b242aa5ff..6dc495247c8 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page])
+ @sort = params[:sort] || 'name'
+ @tags = @repository.tags_sorted_by(@sort)
+ @tags = Kaminari.paginate_array(@tags).page(params[:page])
+
@releases = project.releases.where(tag: @tags)
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
new file mode 100644
index 00000000000..23868d986e9
--- /dev/null
+++ b/app/controllers/projects/todos_controller.rb
@@ -0,0 +1,31 @@
+class Projects::TodosController < Projects::ApplicationController
+ before_action :authenticate_user!, only: [:create]
+
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
+
+ render json: {
+ count: current_user.todos_pending_count,
+ delete_path: dashboard_todo_path(todo)
+ }
+ end
+
+ private
+
+ def issuable
+ @issuable ||= begin
+ case params[:issuable_type]
+ when "issue"
+ issue = @project.issues.find(params[:issuable_id])
+
+ if can?(current_user, :read_issue, issue)
+ issue
+ else
+ render_404
+ end
+ when "merge_request"
+ @project.merge_requests.find(params[:issuable_id])
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 00234654578..6f068729390 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -3,20 +3,44 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
+ def index
+ @variable = Ci::Variable.new
+ end
+
def show
+ @variable = @project.variables.find(params[:id])
end
def update
- if project.update_attributes(project_params)
+ @variable = @project.variables.find(params[:id])
+
+ if @variable.update_attributes(project_params)
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully updated.'
+ else
+ render action: "show"
+ end
+ end
+
+ def create
+ @variable = Ci::Variable.new(project_params)
+
+ if @variable.valid? && @project.variables << @variable
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
else
- render action: 'show'
+ render action: "index"
end
end
+ def destroy
+ @key = @project.variables.find(params[:id])
+ @key.destroy
+
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
+ end
+
private
def project_params
- params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
+ params.require(:variable).permit([:id, :key, :value, :_destroy])
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 0d6c32fabd2..607fe9c7fed 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
+ response.headers['Content-Security-Policy'] = "default-src 'none'"
+ response.headers['X-Content-Security-Policy'] = "default-src 'none'"
+
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
@@ -91,11 +94,11 @@ class Projects::WikisController < Projects::ApplicationController
def markdown_preview
text = params[:text]
- ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
- ext.analyze(text)
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user)
+ ext.analyze(text, author: current_user)
render json: {
- body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
+ body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: ext.users.map(&:username)
}
@@ -121,5 +124,4 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params[:wiki].slice(:title, :content, :format, :message)
end
-
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3768efe142a..12e0d5a8413 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,13 +1,14 @@
class ProjectsController < Projects::ApplicationController
include ExtractsPath
- before_action :authenticate_user!, except: [:show, :activity]
+ before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
- before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
+ before_action :assign_ref_vars, only: [:show], if: :repo_exists?
+ before_action :tree, only: [:show], if: :project_view_files?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
+ before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -101,13 +102,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
- if current_user
- @membership = @project.team.find_member(current_user.id)
-
- if @membership
- @notification_setting = current_user.notification_settings_for(@project)
- end
- end
+ @notification_setting = current_user.notification_settings_for(@project) if current_user
if @project.repository_exists?
if @project.empty_repo?
@@ -145,9 +140,11 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
- emojis: AwardEmoji.urls,
+ emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
+ milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
+ labels: autocomplete.labels,
members: participants
}
@@ -190,6 +187,48 @@ class ProjectsController < Projects::ApplicationController
)
end
+ def export
+ @project.add_export_job(current_user: current_user)
+
+ redirect_to(
+ edit_project_path(@project),
+ notice: "Project export started. A download link will be sent by email."
+ )
+ end
+
+ def download_export
+ export_project_path = @project.export_project_path
+
+ if export_project_path
+ send_file export_project_path, disposition: 'attachment'
+ else
+ redirect_to(
+ edit_project_path(@project),
+ alert: "Project export link has expired. Please generate a new export from your project settings."
+ )
+ end
+ end
+
+ def remove_export
+ if @project.remove_exports
+ flash[:notice] = "Project export has been deleted."
+ else
+ flash[:alert] = "Project export could not be deleted."
+ end
+ redirect_to(edit_project_path(@project))
+ end
+
+ def generate_new_export
+ if @project.remove_exports
+ export
+ else
+ redirect_to(
+ edit_project_path(@project),
+ alert: "Project export could not be deleted."
+ )
+ end
+ end
+
def toggle_star
current_user.toggle_star(@project)
@project.reload
@@ -202,8 +241,8 @@ class ProjectsController < Projects::ApplicationController
def markdown_preview
text = params[:text]
- ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
- ext.analyze(text)
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user)
+ ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text),
@@ -213,6 +252,24 @@ class ProjectsController < Projects::ApplicationController
}
end
+ def refs
+ options = {
+ 'Branches' => @repository.branch_names,
+ }
+
+ unless @repository.tag_count.zero?
+ options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ end
+
+ # If reference is commit id - we should add it to branch/tag selectbox
+ ref = Addressable::URI.unescape(params[:ref])
+ if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
+ options['Commits'] = [ref]
+ end
+
+ render json: options.to_json
+ end
+
private
def determine_layout
@@ -235,10 +292,11 @@ class ProjectsController < Projects::ApplicationController
def project_params
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token,
- :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
+ :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_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,
+ :public_builds, :only_allow_merge_if_build_succeeds
)
end
@@ -246,8 +304,18 @@ class ProjectsController < Projects::ApplicationController
project.repository_exists? && !project.empty_repo?
end
- # Override get_id from ExtractsPath, which returns the branch and file path
+ def project_view_files?
+ current_user && current_user.project_view == 'files'
+ end
+
+ # Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
+ # This way we avoid to access the repository.ref_names.
+ def extract_ref(_id)
+ [get_id, '']
+ end
+
+ # Override get_id from ExtractsPath in this case is just the root of the default branch.
def get_id
project.repository.root_ref
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 352bff19383..75b78a49eab 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -37,8 +37,8 @@ class RegistrationsController < Devise::RegistrationsController
super
end
- def after_sign_up_path_for(_resource)
- users_almost_there_path
+ def after_sign_up_path_for(user)
+ user.confirmed? ? dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(_resource)
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index c29f4609e93..17aed816cbd 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,5 +1,6 @@
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
+ include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
skip_before_action :check_2fa_requirement, only: [:destroy]
@@ -13,6 +14,7 @@ class SessionsController < Devise::SessionsController
before_action :load_recaptcha
def new
+ set_minimum_password_length
if Gitlab.config.ldap.enabled
@ldap_servers = Gitlab::LDAP::Config.servers
else
@@ -29,8 +31,7 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
- authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
- log_audit_event(current_user, with: authenticated_with)
+ log_audit_event(current_user, with: authentication_method)
end
end
@@ -39,7 +40,7 @@ class SessionsController < Devise::SessionsController
# 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
+ return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
user = User.admins.last
@@ -53,7 +54,7 @@ class SessionsController < Devise::SessionsController
end
def user_params
- params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
+ params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end
def find_user
@@ -88,26 +89,6 @@ class SessionsController < Devise::SessionsController
find_user.try(:two_factor_enabled?)
end
- def authenticate_with_two_factor
- user = self.resource = find_user
-
- if user_params[:otp_attempt].present? && session[:otp_user_id]
- if valid_otp_attempt?(user)
- # Remove any lingering user data from login
- session.delete(:otp_user_id)
-
- sign_in(user) and return
- else
- flash.now[:alert] = 'Invalid two-factor code.'
- render :two_factor and return
- end
- else
- if user && user.valid_password?(user_params[:password])
- prompt_for_two_factor(user)
- end
- end
- end
-
def auto_sign_in_with_provider
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
@@ -136,4 +117,14 @@ class SessionsController < Devise::SessionsController
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
+
+ def authentication_method
+ if user_params[:otp_attempt]
+ "two-factor"
+ elsif user_params[:device_response]
+ "two-factor-via-u2f-device"
+ else
+ "standard"
+ end
+ end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2daceed039b..2a17c1f34db 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -10,7 +10,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
- skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw]
+ skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets'
respond_to :html
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2ae180c8a12..a99632454d9 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -58,11 +58,22 @@ class UsersController < ApplicationController
end
end
+ def snippets
+ load_snippets
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("snippets/_snippets", collection: @snippets)
+ }
+ end
+ end
+ end
+
def calendar
calendar = contributions_calendar
@timestamps = calendar.timestamps
- @starting_year = calendar.starting_year
- @starting_month = calendar.starting_month
render 'calendar', layout: false
end
@@ -116,6 +127,15 @@ class UsersController < ApplicationController
@groups = JoinedGroupsFinder.new(user).execute(current_user)
end
+ def load_snippets
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
+ filter: :by_user,
+ user: user,
+ scope: params[:scope]
+ ).page(params[:page])
+ end
+
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index 3b9a421b118..aa8f4c1d0e4 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder
projects = []
if current_user
- if @group.users.include?(current_user)
+ if @group.users.include?(current_user) || current_user.admin?
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f00f3f709e9..a0932712bd0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -224,7 +224,7 @@ class IssuableFinder
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
- params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end
def by_assignee(items)
@@ -250,12 +250,12 @@ class IssuableFinder
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
- items = items.where(milestone_id: [-1, nil])
+ items = items.left_joins_milestones.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.try(:title) })
+ upcoming_ids = Milestone.upcoming_ids_by_projects(projects)
+ items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
else
- items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
+ items = items.with_milestone(params[:milestone_title])
if projects
items = items.where(milestones: { project_id: projects })
@@ -271,7 +271,7 @@ class IssuableFinder
if filter_by_no_label?
items = items.without_label
else
- items = items.with_label(label_names)
+ items = items.with_label(label_names, params[:sort])
if projects
items = items.where(labels: { project_id: projects })
end
@@ -318,7 +318,11 @@ class IssuableFinder
end
def label_names
- params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
+ if labels?
+ params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
+ else
+ []
+ end
end
def current_user_related?
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index fa4c635f55c..0b7832e6583 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -10,11 +10,11 @@ class NotesFinder
notes =
case target_type
when "commit"
- project.notes.for_commit_id(target_id).not_inline
+ project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
- project.issues.find(target_id).notes.nonawards.inc_author
+ project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
+ project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
new file mode 100644
index 00000000000..641fbf838f1
--- /dev/null
+++ b/app/finders/pipelines_finder.rb
@@ -0,0 +1,38 @@
+class PipelinesFinder
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute(pipelines, scope)
+ case scope
+ when 'running'
+ pipelines.running_or_pending
+ when 'branches'
+ from_ids(pipelines, ids_for_ref(pipelines, branches))
+ when 'tags'
+ from_ids(pipelines, ids_for_ref(pipelines, tags))
+ else
+ pipelines
+ end
+ end
+
+ private
+
+ def ids_for_ref(pipelines, refs)
+ pipelines.where(ref: refs).group(:ref).select('max(id)')
+ end
+
+ def from_ids(pipelines, ids)
+ pipelines.unscoped.where(id: ids)
+ end
+
+ def branches
+ project.repository.branch_names
+ end
+
+ def tags
+ project.repository.tag_names
+ end
+end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 01cbf91c658..00ff1611039 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user.id) || current_user.admin?
+ if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 3ba27c40504..7806d9e4cc5 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -25,24 +25,37 @@ class TodosFinder
def execute
items = current_user.todos
items = by_action_id(items)
+ items = by_action(items)
items = by_author(items)
items = by_project(items)
items = by_state(items)
items = by_type(items)
- items
+ items.reorder(id: :desc)
end
private
def action_id?
- action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
+ action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end
def action_id
params[:action_id]
end
+ def to_action_id
+ Todo::ACTION_NAMES.key(action.to_sym)
+ end
+
+ def action?
+ action.present? && to_action_id
+ end
+
+ def action
+ params[:action]
+ end
+
def author?
params[:author_id].present?
end
@@ -78,6 +91,16 @@ class TodosFinder
@project
end
+ def projects
+ return @projects if defined?(@projects)
+
+ if project?
+ @projects = project
+ else
+ @projects = ProjectsFinder.new.execute(current_user)
+ end
+ end
+
def type?
type.present? && ['Issue', 'MergeRequest'].include?(type)
end
@@ -86,6 +109,14 @@ class TodosFinder
params[:type]
end
+ def by_action(items)
+ if action?
+ items = items.where(action: to_action_id)
+ end
+
+ items
+ end
+
def by_action_id(items)
if action_id?
items = items.where(action: action_id)
@@ -105,13 +136,15 @@ class TodosFinder
def by_project(items)
if project?
items = items.where(project: project)
+ elsif projects
+ items = items.merge(projects).joins(:project)
end
items
end
def by_state(items)
- case params[:state]
+ case params[:state].to_s
when 'done'
items.done
else
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index e0abc3a2869..950f323e383 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -30,4 +30,8 @@ module AppearancesHelper
render 'shared/logo.svg'
end
end
+
+ def navbar_icon(icon_name, size: 16)
+ render "shared/icons/#{icon_name}.svg", size: size
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3e0074da394..03495cf5ec4 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -101,23 +101,6 @@ module ApplicationHelper
'Never'
end
- def grouped_options_refs
- repository = @project.repository
-
- options = [
- ['Branches', repository.branch_names],
- ['Tags', VersionSorter.rsort(repository.tag_names)]
- ]
-
- # If reference is commit id - we should add it to branch/tag selectbox
- if(@ref && !options.flatten.include?(@ref) &&
- @ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
- options << ['Commit', [@ref]]
- end
-
- grouped_options_for_select(options, @ref || @project.default_branch)
- end
-
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
@@ -133,7 +116,7 @@ module ApplicationHelper
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
- return false unless project.repository.branch_names.include?(event.branch_name)
+ return false unless project.repository.branch_exists?(event.branch_name)
true
end
@@ -214,7 +197,7 @@ module ApplicationHelper
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
- Haml::Helpers.preserve(markdown(file_content))
+ Hamlit::RailsHelpers.preserve(markdown(file_content))
elsif asciidoc?(file_name)
asciidoc(file_content)
elsif plain?(file_name)
@@ -263,6 +246,8 @@ module ApplicationHelper
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
+ issue_search: params[:issue_search],
+ label_name: params[:label_name]
}
options = exist_opts.merge(options)
@@ -273,16 +258,11 @@ module ApplicationHelper
end
end
- path = request.path
- path << "?#{options.to_param}"
- if add_label
- if params[:label_name].present? and params[:label_name].respond_to?('any?')
- params[:label_name].each do |label|
- path << "&label_name[]=#{label}"
- end
- end
- end
- path
+ params = options.compact
+
+ params.delete(:label_name) unless add_label
+
+ "#{request.path}?#{params.to_param}"
end
def outdated_browser?
@@ -326,4 +306,15 @@ module ApplicationHelper
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
+
+ # While similarly named to Rails's `link_to_if`, this method behaves quite differently.
+ # If `condition` is truthy, a link will be returned with the result of the block
+ # as its body. If `condition` is falsy, only the result of the block will be returned.
+ def conditional_link_to(condition, options, html_options = {}, &block)
+ if condition
+ link_to options, html_options, &block
+ else
+ capture(&block)
+ end
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 914b0ef6042..78c0b79d2bd 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -15,6 +15,10 @@ module ApplicationSettingsHelper
current_application_settings.sign_in_text
end
+ def after_sign_up_text
+ current_application_settings.after_sign_up_text
+ end
+
def shared_runners_text
current_application_settings.shared_runners_text
end
@@ -27,6 +31,28 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
+ def allowed_protocols_present?
+ current_application_settings.enabled_git_access_protocol.present?
+ end
+
+ def enabled_protocol
+ case current_application_settings.enabled_git_access_protocol
+ when 'http'
+ gitlab_config.protocol
+ when 'ssh'
+ 'ssh'
+ end
+ end
+
+ def enabled_project_button(project, protocol)
+ case protocol
+ when 'ssh'
+ ssh_clone_button(project, 'bottom', append_link: false)
+ else
+ http_clone_button(project, 'bottom', append_link: false)
+ end
+ end
+
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
@@ -60,4 +86,26 @@ module ApplicationSettingsHelper
end
end
end
+
+ def oauth_providers_checkboxes
+ button_based_providers.map do |source|
+ disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s)
+ css_class = 'btn'
+ css_class << ' active' unless disabled
+ checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
+
+ label_tag(checkbox_name, class: css_class) do
+ check_box_tag(checkbox_name, source, !disabled,
+ autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source)
+ end
+ end
+ end
+
+ def repository_storage_options_for_select
+ options = Gitlab.config.repositories.storages.map do |name, path|
+ ["#{name} - #{path}", name]
+ end
+
+ options_for_select(options, @application_setting.repository_storage)
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index b4f80fd9b3e..cd4d778e508 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -38,6 +38,16 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
+ def enabled_button_based_providers
+ disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || []
+
+ button_based_providers.map(&:to_s) - disabled_providers
+ end
+
+ def button_based_providers_enabled?
+ enabled_button_based_providers.any?
+ end
+
def provider_image_tag(provider, size = 64)
label = label_for_provider(provider)
@@ -56,7 +66,7 @@ module AuthHelper
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
- !current_user.two_factor_enabled &&
+ !current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 93241b3afb7..428a42266d0 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,10 +1,10 @@
module BlobHelper
- def highlighter(blob_name, blob_content, nowrap: false)
- Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap)
+ def highlighter(blob_name, blob_content, repository: nil, nowrap: false)
+ Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap, repository: repository)
end
- def highlight(blob_name, blob_content, nowrap: false, plain: false)
- Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain)
+ def highlight(blob_name, blob_content, repository: nil, nowrap: false, plain: false)
+ Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain, repository: repository)
end
def no_highlight_files
@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
- link_to "Edit", edit_path, class: 'btn btn-file-option'
+ link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
@@ -116,7 +116,7 @@ module BlobHelper
end
def blob_text_viewable?(blob)
- blob && blob.text? && !blob.lfs_pointer?
+ blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end
def blob_size(blob)
@@ -180,8 +180,22 @@ module BlobHelper
licenses = Licensee::License.all
@licenses_for_select = {
- Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
- Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
+ Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
+ Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
end
+
+ def gitignore_names
+ @gitignore_names ||=
+ Gitlab::Template::Gitignore.categories.keys.map do |k|
+ [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
+ end
+
+ def gitlab_ci_ymls
+ @gitlab_ci_ymls ||=
+ Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
+ [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
+ end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index e39548e17e1..601df5c18df 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -10,8 +10,12 @@ module BranchesHelper
end
def can_push_branch?(project, branch_name)
- return false unless project.repository.branch_names.include?(branch_name)
+ return false unless project.repository.branch_exists?(branch_name)
- ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
+ ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(branch_name)
+ end
+
+ def project_branches
+ options_for_select(@project.repository.branch_names, @project.default_branch)
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index a9047ede8c5..0f097f86816 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,38 +17,56 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-clipboard',
+ class: "btn btn-clipboard",
data: data,
type: :button
end
- def http_clone_button(project)
+ # Output a "Copy to Clipboard" button with a custom CSS class
+ #
+ # data - Data attributes passed to `content_tag`
+ # css_class - Class passed to the `content_tag`
+ #
+ # Examples:
+ #
+ # # Define the target element
+ # clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
+ # # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
+ def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
+ content_tag :button,
+ icon('clipboard'),
+ class: "btn #{css_class}",
+ data: data,
+ type: :button
+ end
+
+ def http_clone_button(project, placement = 'right', append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase
- content_tag :a, protocol,
+ content_tag (append_link ? :a : :span), protocol,
class: klass,
- href: @project.http_url_to_repo,
+ href: (project.http_url_to_repo if append_link),
data: {
html: true,
- placement: 'right',
+ placement: placement,
container: 'body',
title: "Set a password on your account<br>to pull or push via #{protocol}"
}
end
- def ssh_clone_button(project)
+ def ssh_clone_button(project, placement = 'right', append_link: true)
klass = 'ssh-selector'
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
- content_tag :a, 'SSH',
+ content_tag (append_link ? :a : :span), 'SSH',
class: klass,
- href: project.ssh_url_to_repo,
+ href: (project.ssh_url_to_repo if append_link),
data: {
html: true,
- placement: 'right',
+ placement: placement,
container: 'body',
title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
}
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 417050b4132..8e4ae1e6aec 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,7 +1,7 @@
module CiStatusHelper
- def ci_status_path(ci_commit)
- project = ci_commit.project
- builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
+ def ci_status_path(pipeline)
+ project = pipeline.project
+ builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
end
def ci_status_with_icon(status, target = nil)
@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_ci_status(ci_commit, tooltip_placement: 'auto left')
- # TODO: split this method into
- # - render_commit_status
- # - render_pipeline_status
- link_to ci_icon_for_status(ci_commit.status),
- ci_status_path(ci_commit),
- class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
- title: "Build #{ci_label_for_status(ci_commit.status)}",
- data: { toggle: 'tooltip', placement: tooltip_placement }
+ def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
+ project = commit.project
+ path = builds_namespace_project_commit_path(project.namespace, project, commit)
+ render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
+ end
+
+ def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
+ project = pipeline.project
+ path = namespace_project_pipeline_path(project.namespace, project, pipeline)
+ render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
end
+
+ private
+
+ def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
+ link_to ci_icon_for_status(status),
+ path,
+ class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
+ title: "#{type.titleize}: #{ci_label_for_status(status)}",
+ data: { toggle: 'tooltip', placement: tooltip_placement }
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index b59c3982edd..474041eccbb 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
+ def commit_author_avatar(commit, options = {})
+ options = options.merge(source: :author)
+ user = commit.send(options[:source])
+
+ source_email = clean(commit.send "#{options[:source]}_email".to_sym)
+ person_email = user.try(:email) || source_email
+
+ image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
+ end
+
def image_diff_class(diff)
if diff.deleted_file
"deleted"
@@ -102,34 +112,35 @@ module CommitsHelper
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
- "Browse File »",
+ "Browse File",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
- class: "pull-right"
+ class: "btn btn-default"
)
elsif @path.present?
return link_to(
- "Browse Directory »",
+ "Browse Directory",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
- class: "pull-right"
+ class: "btn btn-default"
)
end
end
link_to(
"Browse Files",
namespace_project_tree_path(project.namespace, project, commit),
- class: "pull-right"
+ class: "btn btn-default"
)
end
- def revert_commit_link(commit, continue_to_path, btn_class: nil)
+ def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
- tooltip = "Revert this #{commit.change_type_title} in a new merge request"
+ tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
if can_collaborate_with_project?
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
+ btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
@@ -140,17 +151,20 @@ module CommitsHelper
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
+ btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
+
+ link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
end
- def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil)
+ def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
- link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
+ btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
+ link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
@@ -161,7 +175,8 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
- link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
+ btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
+ link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
end
@@ -182,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
- person_email = user.try(:email) || source_email
text =
if options[:avatar]
- avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
- %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
+ %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 9f73edb4553..eb57516247d 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -2,8 +2,8 @@ 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 = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion)
+ marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition)
[marked_old_line, marked_new_line]
end
@@ -30,20 +30,16 @@ module DiffHelper
options
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)
+ def safe_diff_files(diffs, diff_refs: nil, repository: nil)
+ diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
end
def unfold_bottom_class(bottom)
- (bottom) ? 'js-unfold-bottom' : ''
+ bottom ? 'js-unfold-bottom' : ''
end
def unfold_class(unfold)
- (unfold) ? 'unfold js-unfold' : ''
+ unfold ? 'unfold js-unfold' : ''
end
def diff_line_content(line, line_type = nil)
@@ -55,22 +51,18 @@ module DiffHelper
end
end
- def line_comments
- @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)
- comments_left = comments_right = nil
+ def organize_comments(left, right)
+ notes_left = notes_right = nil
- unless type_left.nil? && type_right == 'new'
- comments_left = line_comments[line_code_left]
+ unless left[:type].nil? && right[:type] == 'new'
+ notes_left = @grouped_diff_notes[left[:line_code]]
end
- unless type_left.nil? && type_right.nil?
- comments_right = line_comments[line_code_right]
+ unless left[:type].nil? && right[:type].nil?
+ notes_right = @grouped_diff_notes[right[:line_code]]
end
- [comments_left, comments_right]
+ [notes_left, notes_right]
end
def inline_diff_btn
@@ -96,18 +88,21 @@ module DiffHelper
].join(' ').html_safe
end
- def commit_for_diff(diff)
- if diff.deleted_file
+ def commit_for_diff(diff_file)
+ return diff_file.content_commit if diff_file.content_commit
+
+ if diff_file.deleted_file
@base_commit || @commit.parent || @commit
else
@commit
end
end
- def diff_file_html_data(project, diff_commit, diff_file)
+ def diff_file_html_data(project, diff_file)
+ commit = commit_for_diff(diff_file)
{
blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
- tree_join(diff_commit.id, diff_file.file_path))
+ tree_join(commit.id, diff_file.file_path))
}
end
@@ -139,6 +134,11 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
+ def diff_compare_whitespace_link(project, from, to, options)
+ url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
+ toggle_whitespace_link(url, options)
+ end
+
private
def hide_whitespace?
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 14697f774cc..4566f3782cc 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -39,7 +39,7 @@ module DropdownsHelper
end
end
- def dropdown_toggle(toggle_text, data_attr, options)
+ 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')
@@ -67,9 +67,9 @@ module DropdownsHelper
end
end
- def dropdown_filter(placeholder)
+ def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
+ filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 41b5bd7be90..2843ad96efa 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,5 +1,4 @@
module EmailsHelper
-
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
@@ -32,12 +31,6 @@ module EmailsHelper
nil
end
- def color_email_diff(diffcontent)
- formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
- lexer = Rouge::Lexers::Diff
- raw formatter.format(lexer.lex(diffcontent))
- end
-
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 592bad8ba24..bfedcb1c42b 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), title: h(author.name)
+ link_to author.name, user_path(author.username), title: author.name
else
event.author_name
end
@@ -39,15 +39,6 @@ module EventsHelper
end
end
- def icon_for_event
- {
- EventFilter.push => 'upload',
- EventFilter.merged => 'check-square-o',
- EventFilter.comments => 'comments',
- EventFilter.team => 'user',
- }
- end
-
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
@@ -66,11 +57,7 @@ module EventsHelper
words << event.ref_name
words << "at"
elsif event.commented?
- if event.note_commit?
- words << event.note_short_commit_id
- else
- words << "##{truncate event.note_target_iid}"
- end
+ words << event.note_target_reference
words << "at"
elsif event.milestone?
words << "##{event.target_iid}" if event.target_iid
@@ -93,21 +80,12 @@ module EventsHelper
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
- elsif event.note? && event.note_commit?
+ elsif event.note? && event.commit_note?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
if event.note_target
- if event.note_commit?
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_commit_id,
- anchor: dom_id(event.target))
- elsif event.note_project_snippet?
- namespace_project_snippet_path(event.project.namespace,
- event.project, event.note_target)
- else
- event_note_target_path(event)
- end
+ event_note_target_path(event)
end
elsif event.push?
push_event_feed_url(event)
@@ -143,42 +121,30 @@ module EventsHelper
end
def event_note_target_path(event)
- if event.note? && event.note_commit?
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_target)
+ if event.note? && event.commit_note?
+ namespace_project_commit_path(event.project.namespace,
+ event.project,
+ event.note_target,
+ anchor: dom_id(event.target))
+ elsif event.project_snippet_note?
+ namespace_project_snippet_path(event.project.namespace,
+ event.project,
+ event.note_target,
+ anchor: dom_id(event.target))
else
polymorphic_path([event.project.namespace.becomes(Namespace),
event.project, event.note_target],
- anchor: dom_id(event.target))
+ anchor: dom_id(event.target))
end
end
def event_note_title_html(event)
if event.note_target
- if event.note_commit?
- link_to(
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_commit_id,
- anchor: dom_id(event.target), title: h(event.target_title)),
- class: "commit_short_id"
- ) do
- "#{event.note_target_type} #{event.note_short_commit_id}"
- end
- elsif event.note_project_snippet?
- link_to(namespace_project_snippet_path(event.project.namespace,
- event.project,
- 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.to_reference}"
- end
+ link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do
+ "#{event.note_target_type} #{event.note_target_reference}"
end
else
- content_tag :strong do
- "(deleted)"
- end
+ content_tag(:strong, '(deleted)')
end
end
@@ -193,28 +159,6 @@ module EventsHelper
"--broken encoding"
end
- def event_to_atom(xml, event)
- if event.visible_to_user?(current_user)
- xml.entry do
- event_link = event_feed_url(event)
- event_title = event_feed_title(event)
- event_summary = event_feed_summary(event)
-
- xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
- xml.link href: event_link
- xml.title truncate(event_title, length: 80)
- xml.updated event.created_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
- xml.author do |author|
- xml.name event.author_name
- xml.email event.author_email
- end
-
- xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? }
- end
- end
- end
-
def event_row_class(event)
if event.body?
"event-block"
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 3a45205563e..1a259656f31 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,7 +13,7 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
- escaped_body = if body =~ /\A\<img/
+ escaped_body = if body.start_with?('<img')
body
else
escape_once(body)
@@ -50,8 +50,6 @@ module GitlabMarkdownHelper
context[:project] ||= @project
- text = Banzai.pre_process(text, context)
-
html = Banzai.render(text, context)
context.merge!(
@@ -108,7 +106,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
- markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
+ markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
when :asciidoc
asciidoc(wiki_page.content)
else
@@ -185,4 +183,17 @@ module GitlabMarkdownHelper
''
end
end
+
+ def markdown_toolbar_button(options = {})
+ data = options[:data].merge({ container: "body" })
+ content_tag :button,
+ type: "button",
+ class: "toolbar-btn js-md has-tooltip hidden-xs",
+ tabindex: -1,
+ data: data,
+ title: options[:title],
+ aria: { label: options[:title] } do
+ icon(options[:icon])
+ end
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f07eff3fb57..5386ddadc62 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
+ # Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
+ def project_url(project, *args)
+ namespace_project_url(project.namespace, project, *args)
+ end
+
+ def edit_project_path(project, *args)
+ edit_namespace_project_path(project.namespace, project, *args)
+ end
+
+ def edit_project_url(project, *args)
+ edit_namespace_project_url(project.namespace, project, *args)
+ end
+
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
@@ -29,16 +42,20 @@ module GitlabRoutingHelper
namespace_project_pipelines_path(project.namespace, project, *args)
end
+ def project_environments_path(project, *args)
+ namespace_project_environments_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
- def activity_project_path(project, *args)
- activity_namespace_project_path(project.namespace, project, *args)
+ def project_container_registry_path(project, *args)
+ namespace_project_container_registry_index_path(project.namespace, project, *args)
end
- def edit_project_path(project, *args)
- edit_namespace_project_path(project.namespace, project, *args)
+ def activity_project_path(project, *args)
+ activity_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args)
@@ -61,14 +78,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
- def project_url(project, *args)
- namespace_project_url(project.namespace, project, *args)
- end
-
- def edit_project_url(project, *args)
- edit_namespace_project_url(project.namespace, project, *args)
- end
-
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
@@ -88,4 +97,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
+
+ ## Members
+ def project_members_url(project, *args)
+ namespace_project_project_members_url(project.namespace, project)
+ end
+
+ def project_member_path(project_member, *args)
+ namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ def request_access_project_members_path(project, *args)
+ request_access_namespace_project_project_members_path(project.namespace, project)
+ end
+
+ def leave_project_members_path(project, *args)
+ leave_namespace_project_project_members_path(project.namespace, project)
+ end
+
+ def approve_access_request_project_member_path(project_member, *args)
+ approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ def resend_invite_project_member_path(project_member, *args)
+ resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ # Groups
+
+ ## Members
+ def group_members_url(group, *args)
+ group_group_members_url(group, *args)
+ end
+
+ def group_member_path(group_member, *args)
+ group_group_member_path(group_member.source, group_member)
+ end
+
+ def request_access_group_members_path(group, *args)
+ request_access_group_group_members_path(group)
+ end
+
+ def leave_group_members_path(group, *args)
+ leave_group_group_members_path(group)
+ end
+
+ def approve_access_request_group_member_path(group_member, *args)
+ approve_access_request_group_group_member_path(group_member.source, group_member)
+ end
+
+ def resend_invite_group_member_path(group_member, *args)
+ resend_invite_group_group_member_path(group_member.source, group_member)
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b1f0a765bb9..b9211e88473 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,24 +1,4 @@
module GroupsHelper
- def remove_user_from_group_message(group, member)
- if member.user
- "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
- else
- "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
- end
- end
-
- def leave_group_message(group)
- "Are you sure you want to leave \"#{group}\" group?"
- end
-
- def should_user_see_group_roles?(user, group)
- if user
- user.is_admin? || group.members.exists?(user_id: user.id)
- else
- false
- end
- end
-
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
@@ -31,7 +11,7 @@ module GroupsHelper
if group && group.avatar.present?
group.avatar.url
else
- 'no_group_avatar.png'
+ image_path('no_group_avatar.png')
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 39474217286..294b7e92b8d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -1,5 +1,4 @@
module IssuablesHelper
-
def sidebar_gutter_toggle_icon
sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right')
end
@@ -8,14 +7,6 @@ module IssuablesHelper
"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 multi_label_name(current_labels, default_label)
# current_labels may be a string from before
if current_labels.is_a?(Array)
@@ -45,19 +36,11 @@ module IssuablesHelper
end
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 default_label if user_id.nil?
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
+ user = User.find_by(id: user_id)
if user
user.name
@@ -76,13 +59,19 @@ module IssuablesHelper
def issuable_meta(issuable, project, text)
output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
- output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe
+ output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
end
+ def issuable_todo(issuable)
+ if current_user
+ current_user.todos.find_by(target: issuable, state: :pending)
+ end
+ end
+
private
def sidebar_gutter_collapsed?
@@ -100,5 +89,4 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
-
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 198d39455d7..2b0defd1dda 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -105,23 +105,6 @@ module IssuesHelper
return 'hidden' if issue.closed? == closed
end
- def issue_to_atom(xml, issue)
- xml.entry do
- xml.id namespace_project_issue_url(issue.project.namespace,
- issue.project, issue)
- 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.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
- xml.email issue.author_email
- end
- xml.summary issue.title
- end
- end
-
def merge_requests_sentence(merge_requests)
# Sorting based on the `!123` or `group/project!123` reference will sort
# local merge requests first.
@@ -135,7 +118,7 @@ module IssuesHelper
end
def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
- unicode ||= Emoji.emoji_filename(name) rescue ""
+ unicode ||= Gitlab::Emoji.emoji_filename(name) rescue ""
data = {
aliases: aliases.join(" "),
@@ -162,16 +145,14 @@ module IssuesHelper
end
end
- def emoji_author_list(notes, current_user)
- list = notes.map do |note|
- note.author == current_user ? "me" : note.author.name
- end
-
- list.join(", ")
+ def award_user_list(awards, current_user)
+ awards.map do |award|
+ award.user == current_user ? 'me' : award.user.name
+ end.join(', ')
end
- def note_active_class(notes, current_user)
- if current_user && notes.pluck(:author_id).include?(current_user.id)
+ def award_active_class(awards, current_user)
+ if current_user && awards.find { |a| a.user_id == current_user.id }
"active"
else
""
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
new file mode 100644
index 00000000000..0e456214d37
--- /dev/null
+++ b/app/helpers/javascript_helper.rb
@@ -0,0 +1,5 @@
+module JavascriptHelper
+ def page_specific_javascript_tag(js)
+ javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
+ end
+end
diff --git a/app/helpers/kerberos_spnego_helper.rb b/app/helpers/kerberos_spnego_helper.rb
new file mode 100644
index 00000000000..f5b0aa7549a
--- /dev/null
+++ b/app/helpers/kerberos_spnego_helper.rb
@@ -0,0 +1,9 @@
+module KerberosSpnegoHelper
+ def allow_basic_auth?
+ true # different behavior in GitLab Enterprise Edition
+ end
+
+ def allow_kerberos_spnego_auth?
+ false # different behavior in GitLab Enterprise Edition
+ end
+end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c99b137cdaa..5e9f5837101 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -32,20 +32,24 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, type: :issue, tooltip: true, &block)
+ def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project
- link = send("namespace_project_#{type.to_s.pluralize}_path",
- project.namespace,
- project,
- label_name: [label.name])
+ link = label_filter_path(project, label, type: type)
if block_given?
- link_to link, &block
+ link_to link, class: css_class, &block
else
- link_to render_colored_label(label, tooltip: tooltip), link
+ link_to render_colored_label(label, tooltip: tooltip), link, class: css_class
end
end
+ def label_filter_path(project, label, type: issue)
+ send("namespace_project_#{type.to_s.pluralize}_path",
+ project.namespace,
+ project,
+ label_name: [label.name])
+ end
+
def project_label_names
@project.labels.pluck(:title)
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
new file mode 100644
index 00000000000..ec106418f2d
--- /dev/null
+++ b/app/helpers/members_helper.rb
@@ -0,0 +1,45 @@
+module MembersHelper
+ # Returns a `<action>_<source>_member` association, e.g.:
+ # - admin_project_member, update_project_member, destroy_project_member
+ # - admin_group_member, update_group_member, destroy_group_member
+ def action_member_permission(action, member)
+ "#{action}_#{member.type.underscore}".to_sym
+ end
+
+ def default_show_roles(member)
+ can?(current_user, action_member_permission(:update, member), member) ||
+ can?(current_user, action_member_permission(:destroy, member), member) ||
+ can?(current_user, action_member_permission(:admin, member), member.source)
+ end
+
+ def remove_member_message(member, user: nil)
+ user = current_user if defined?(current_user)
+
+ text = 'Are you sure you want to '
+ action =
+ if member.request?
+ if member.user == user
+ 'withdraw your access request for'
+ else
+ "deny #{member.user.name}'s request to join"
+ end
+ elsif member.invite?
+ "revoke the invitation for #{member.invite_email} to join"
+ else
+ "remove #{member.user.name} from"
+ end
+
+ text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ end
+
+ def remove_member_title(member)
+ text = " from #{member.real_source_type.humanize(capitalize: false)}"
+
+ text.prepend(member.request? ? 'Deny access request' : 'Remove user')
+ end
+
+ def leave_confirmation_message(member_source)
+ "Are you sure you want to leave the " \
+ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
+ end
+end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 1dd07a2a220..c7dedfe9254 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -27,7 +27,7 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
- build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
+ build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch)
return nil unless build_url
parsed_url = URI.parse(build_url)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 87fc2db6901..b3e6e468ecd 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -56,7 +56,7 @@ module MilestonesHelper
def milestone_remaining_days(milestone)
if milestone.expired?
- content_tag(:strong, 'expired')
+ content_tag(:strong, 'Past due')
elsif milestone.due_date
days = milestone.remaining_days
content = content_tag(:strong, days)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 3aa41030453..3ff8be5e284 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -12,10 +12,10 @@ module NavHelper
end
def page_sidebar_class
- if nav_menu_collapsed?
- "page-sidebar-collapsed"
+ if pinned_nav?
+ "page-sidebar-expanded page-sidebar-pinned"
else
- "page-sidebar-expanded"
+ "page-sidebar-collapsed"
end
end
@@ -30,17 +30,33 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
+ elsif current_path?('builds#show')
+ "page-gutter build-sidebar right-sidebar-expanded"
end
end
def nav_header_class
- class_name =
- if nav_menu_collapsed?
- "header-collapsed"
- else
- "header-expanded"
- end
- class_name += " with-horizontal-nav" if defined?(nav) && nav
+ class_name = ''
+ class_name << " with-horizontal-nav" if defined?(nav) && nav
+
+ if pinned_nav?
+ class_name << " header-expanded header-pinned-nav"
+ else
+ class_name << " header-collapsed"
+ end
+
class_name
end
+
+ def layout_nav_class
+ "page-with-layout-nav" if defined?(nav) && nav
+ end
+
+ def nav_control_class
+ "nav-control" if current_user
+ end
+
+ def pinned_nav?
+ cookies[:pin_nav] == 'true'
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 95072b5373f..2302e65c537 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -1,7 +1,7 @@
module NotesHelper
# Helps to distinguish e.g. commit notes in mr notes list
def note_for_main_target?(note)
- (@noteable.class.name == note.noteable_type && !note.for_diff_line?)
+ @noteable.class.name == note.noteable_type && !note.diff_note?
end
def note_target_fields(note)
@@ -15,16 +15,6 @@ module NotesHelper
note.editable? && can?(current_user, :admin_note, note)
end
- def link_to_commit_diff_line_note(note)
- if note.for_commit_diff_line?
- link_to(
- "#{note.diff_file_name}:L#{note.diff_new_line}",
- namespace_project_commit_path(@project.namespace, @project,
- note.noteable, anchor: note.line_code)
- )
- end
- end
-
def noteable_json(noteable)
{
id: noteable.id,
@@ -34,22 +24,55 @@ module NotesHelper
}.to_json
end
- def link_to_new_diff_note(line_code, line_type = nil)
- discussion_id = Note.build_discussion_id(
- @comments_target[:noteable_type],
- @comments_target[:noteable_id] || @comments_target[:commit_id],
- line_code
- )
+ def link_to_new_diff_note(line_code, position, line_type = nil)
+ use_legacy_diff_note = @use_legacy_diff_notes
+ # If the controller doesn't force the use of legacy diff notes, we
+ # determine this on a line-by-line basis by seeing if there already exist
+ # active legacy diff notes at this line, in which case newly created notes
+ # will use the legacy technology as well.
+ # We do this because the discussion_id values of legacy and "new" diff
+ # notes, which are used to group notes on the merge request discussion tab,
+ # are incompatible.
+ # If we didn't, diff notes that would show for the same line on the changes
+ # tab, would show in different discussions on the discussion tab.
+ use_legacy_diff_note ||= begin
+ line_diff_notes = @grouped_diff_notes[line_code]
+ line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?)
+ end
data = {
noteable_type: @comments_target[:noteable_type],
noteable_id: @comments_target[:noteable_id],
commit_id: @comments_target[:commit_id],
- line_code: line_code,
- discussion_id: discussion_id,
- line_type: line_type
+ line_type: line_type,
+ line_code: line_code
}
+ if use_legacy_diff_note
+ discussion_id = LegacyDiffNote.build_discussion_id(
+ @comments_target[:noteable_type],
+ @comments_target[:noteable_id] || @comments_target[:commit_id],
+ line_code
+ )
+
+ data.merge!(
+ note_type: LegacyDiffNote.name,
+ discussion_id: discussion_id
+ )
+ else
+ discussion_id = DiffNote.build_discussion_id(
+ @comments_target[:noteable_type],
+ @comments_target[:noteable_id] || @comments_target[:commit_id],
+ position
+ )
+
+ data.merge!(
+ position: position.to_json,
+ note_type: DiffNote.name,
+ discussion_id: discussion_id
+ )
+ end
+
button_tag(class: 'btn add-diff-note js-add-diff-note-button',
data: data,
title: 'Add a comment to this line') do
@@ -57,19 +80,46 @@ module NotesHelper
end
end
- def link_to_reply_diff(note, line_type = nil)
+ def link_to_reply_discussion(note, line_type = nil)
return unless current_user
data = {
noteable_type: note.noteable_type,
noteable_id: note.noteable_id,
commit_id: note.commit_id,
- line_code: note.line_code,
discussion_id: note.discussion_id,
line_type: line_type
}
- button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
- data: data, title: 'Add a reply'
+ if note.diff_note?
+ data[:note_type] = note.type
+
+ data.merge!(note.diff_attributes)
+ end
+
+ content_tag(:div, class: "discussion-reply-holder") do
+ button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
+ data: data, title: 'Add a reply'
+ end
+ end
+
+ def note_max_access_for_user(note)
+ @max_access_by_user_id ||= Hash.new do |hash, key|
+ project = key[:project]
+ hash[key] = project.team.human_max_access(key[:user_id])
+ end
+
+ full_key = { project: note.project, user_id: note.author_id }
+ @max_access_by_user_id[full_key]
+ end
+
+ def diff_note_path(note)
+ return unless note.diff_note?
+
+ if note.for_merge_request? && note.active?
+ diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
+ elsif note.for_commit?
+ namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
+ end
end
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 54ab9179efc..7e8369d0a05 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -31,6 +31,23 @@ module NotificationsHelper
end
end
+ def notification_description(level)
+ case level.to_sym
+ when :participating
+ 'You will only receive notifications for threads you have participated in'
+ when :mention
+ 'You will receive notifications only for comments in which you were @mentioned'
+ when :watch
+ 'You will receive notifications for any activity'
+ when :disabled
+ 'You will not get any notifications via email'
+ when :global
+ 'Use your global notification setting'
+ when :custom
+ 'You will only receive notifications for the events you choose'
+ end
+ end
+
def notification_list_item(level, setting)
title = notification_title(level)
@@ -39,10 +56,22 @@ module NotificationsHelper
notification_title: title
}
- content_tag(:li, class: ('active' if setting.level == level)) do
- link_to '#', class: 'update-notification', data: data do
- notification_icon(level, title)
+ content_tag(:li, role: "menuitem") do
+ link_to '#', class: "update-notification #{('is-active' if setting.level == level)}", data: data do
+ link_output = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
+ link_output << content_tag(:span, notification_description(level), class: 'dropdown-menu-inner-content')
end
end
end
+
+ # Identifier to trigger individually dropdowns and custom settings modals in the same view
+ def notifications_menu_identifier(type, notification_setting)
+ "#{type}-#{notification_setting.user_id}-#{notification_setting.source_id}-#{notification_setting.source_type}"
+ end
+
+ # Create hidden field to send notification setting source to controller
+ def hidden_setting_source_input(notification_setting)
+ return unless notification_setting.source_type
+ hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
+ end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index e4e8b934bc8..22387d66451 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -52,7 +52,7 @@ module PageLayoutHelper
raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
@page_card_attributes ||= {}
- @page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
+ @page_card_attributes = map.reject { |_, v| v.blank? } if map.present?
@page_card_attributes
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 85f8854d2eb..3bbbb26cff2 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,12 +1,4 @@
module ProjectsHelper
- def remove_from_project_team_message(project, member)
- if member.user
- "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
- else
- "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
- end
- end
-
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
@@ -23,7 +15,7 @@ module ProjectsHelper
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]
+ 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 = {}, &block)
@@ -35,7 +27,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- 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]
+ 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
if opts[:by_username]
@@ -49,7 +41,7 @@ module ProjectsHelper
author_html = author_html.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
+ link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{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", title: title, data: { container: 'body' } ).html_safe
@@ -115,20 +107,8 @@ module ProjectsHelper
end
end
- def user_max_access_in_project(user_id, project)
- level = project.team.max_member_access(user_id)
-
- if level
- Gitlab::Access.options_with_owner.key(level)
- end
- end
-
def license_short_name(project)
- no_license_key = project.repository.license_key.nil? ||
- # Back-compat if cache contains 'no-license', can be removed in a few weeks
- project.repository.license_key == 'no-license'
-
- return 'LICENSE' if no_license_key
+ return 'LICENSE' if project.repository.license_key.nil?
license = Licensee::License.new(project.repository.license_key)
@@ -138,20 +118,32 @@ module ProjectsHelper
private
def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home, :forks]
+ nav_tabs = [:home]
if !project.empty_repo? && can?(current_user, :download_code, project)
- nav_tabs << [:files, :commits, :network, :graphs]
+ nav_tabs << [:files, :commits, :network, :graphs, :forks]
end
if project.repo_exists? && can?(current_user, :read_merge_request, project)
nav_tabs << :merge_requests
end
+ if can?(current_user, :read_pipeline, project)
+ nav_tabs << :pipelines
+ end
+
if can?(current_user, :read_build, project)
nav_tabs << :builds
end
+ if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
+ nav_tabs << :container_registry
+ end
+
+ if can?(current_user, :read_environment, project)
+ nav_tabs << :environments
+ end
+
if can?(current_user, :admin_project, project)
nav_tabs << :settings
end
@@ -214,10 +206,14 @@ module ProjectsHelper
end
def default_clone_protocol
- if !current_user || current_user.require_ssh_key?
- gitlab_config.protocol
+ if allowed_protocols_present?
+ enabled_protocol
else
- "ssh"
+ if !current_user || current_user.require_ssh_key?
+ gitlab_config.protocol
+ else
+ 'ssh'
+ end
end
end
@@ -282,10 +278,6 @@ module ProjectsHelper
end
end
- def leave_project_message(project)
- "Are you sure you want to leave \"#{project.name}\" project?"
- end
-
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
@@ -301,7 +293,11 @@ module ProjectsHelper
end
def last_push_event
- if current_user
+ return unless current_user
+
+ if fork = current_user.fork_of(@project)
+ current_user.recent_push(fork.id)
+ else
current_user.recent_push(@project.id)
end
end
@@ -339,9 +335,9 @@ module ProjectsHelper
end
end
- def sanitize_repo_path(message)
+ def sanitize_repo_path(project, message)
return '' unless message.present?
- message.strip.gsub(Gitlab.config.gitlab_shell.repos_path.chomp('/'), "[REPOS PATH]")
+ message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 24c4c098c65..f9fc525df6f 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,5 +1,4 @@
module SearchHelper
-
def search_autocomplete_opts(term)
return unless current_user
@@ -59,7 +58,7 @@ module SearchHelper
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref
- ref = @ref || @project.repository.root_ref
+ ref = @ref || @project.repository.root_ref
[
{ category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index e951a87a212..bb395e37884 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -18,7 +18,7 @@ module SelectsHelper
first_user: first_user,
current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
- author_id: opts[:author_id] || ''
+ author_id: opts[:author_id] || ''
}
}
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 630e10ea892..d86f1999f5c 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -14,7 +14,8 @@ module SortingHelper
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
+ sort_value_upvotes => sort_title_upvotes,
+ sort_value_priority => sort_title_priority
}
end
@@ -28,6 +29,10 @@ module SortingHelper
}
end
+ def sort_title_priority
+ 'Priority'
+ end
+
def sort_title_oldest_updated
'Oldest updated'
end
@@ -84,6 +89,10 @@ module SortingHelper
'Most popular'
end
+ def sort_value_priority
+ 'priority'
+ end
+
def sort_value_oldest_updated
'updated_asc'
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 96a83671009..563ddd2a511 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -95,7 +95,9 @@ module TabHelper
end
def project_tab_class
- return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
+ if controller.controller_path.start_with?('projects')
+ return 'active'
+ end
if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
"active"
@@ -112,7 +114,7 @@ module TabHelper
end
def profile_tab_class
- if controller.controller_path =~ /\Aprofiles/
+ if controller.controller_path.start_with?('profiles')
return 'active'
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 8142f733e76..8cb82c2d5cc 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -20,8 +20,14 @@ module TimeHelper
end
end
-
def date_from_to(from, to)
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
+
+ def duration_in_numbers(finished_at, started_at)
+ diff_in_seconds = finished_at.to_i - started_at.to_i
+ time_format = diff_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S"
+
+ Time.at(diff_in_seconds).utc.strftime(time_format)
+ end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 2f066682180..a832a6c8df7 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,22 +1,26 @@
module TodosHelper
def todos_pending_count
- current_user.todos.pending.count
+ TodosFinder.new(current_user, state: :pending).execute.count
end
def todos_done_count
- current_user.todos.done.count
+ TodosFinder.new(current_user, state: :done).execute.count
end
def todo_action_name(todo)
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
+ when Todo::BUILD_FAILED then 'The build failed for your'
+ when Todo::MARKED then 'added a todo for'
end
end
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
+ link_to "#{target} #{todo.target_reference}", todo_target_path(todo),
+ class: 'has-tooltip',
+ title: todo.target.title
end
def todo_target_path(todo)
@@ -28,8 +32,21 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
- polymorphic_path([todo.project.namespace.becomes(Namespace),
- todo.project, todo.target], anchor: anchor)
+ path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
+
+ path.unshift(:builds) if todo.build_failed?
+
+ polymorphic_path(path, anchor: anchor)
+ end
+ end
+
+ def todo_target_state_pill(todo)
+ return unless show_todo_state?(todo)
+
+ content_tag(:span, nil, class: 'target-status') do
+ content_tag(:span, nil, class: "status-box status-box-#{todo.target.state.dasherize}") do
+ todo.target.state.capitalize
+ end
end
end
@@ -91,4 +108,10 @@ module TodosHelper
options_from_collection_for_select(types, 'name', 'title', params[:type])
end
+
+ private
+
+ def show_todo_state?(todo)
+ (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state)
+ end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
new file mode 100644
index 00000000000..65598ad9ed3
--- /dev/null
+++ b/app/helpers/workhorse_helper.rb
@@ -0,0 +1,31 @@
+# Helpers to send Git blobs, diffs, patches or archives through Workhorse.
+# Workhorse will also serve files when using `send_file`.
+module WorkhorseHelper
+ # Send a Git blob through Workhorse
+ def send_git_blob(repository, 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
+ end
+
+ # Send a Git diff through Workhorse
+ def send_git_diff(repository, diff_refs)
+ headers.store(*Gitlab::Workhorse.send_git_diff(repository, diff_refs))
+ headers['Content-Disposition'] = 'inline'
+ head :ok
+ end
+
+ # Send a Git patch through Workhorse
+ def send_git_patch(repository, diff_refs)
+ headers.store(*Gitlab::Workhorse.send_git_patch(repository, diff_refs))
+ headers['Content-Disposition'] = 'inline'
+ head :ok
+ end
+
+ # Archive a Git repository and send it through Workhorse
+ def send_git_archive(repository, ref:, format:)
+ headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
+ head :ok
+ end
+end
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index b616add283a..415f6e12885 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -1,4 +1,6 @@
class DeviseMailer < Devise::Mailer
default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>"
default reply_to: Gitlab.config.gitlab.email_reply_to
+
+ layout 'devise_mailer'
end
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
deleted file mode 100644
index 1c43f95dc8c..00000000000
--- a/app/mailers/emails/groups.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-module Emails
- module Groups
- def group_access_granted_email(group_member_id)
- @group_member = GroupMember.find(group_member_id)
- @group = @group_member.group
-
- @target_url = group_url(@group)
- @current_user = @group_member.user
-
- mail(to: @group_member.user.notification_email,
- subject: subject("Access to group was granted"))
- end
-
- def group_member_invited_email(group_member_id, token)
- @group_member = GroupMember.find group_member_id
- @group = @group_member.group
- @token = token
-
- @target_url = group_url(@group)
- @current_user = @group_member.user
-
- mail(to: @group_member.invite_email,
- subject: "Invitation to join group #{@group.name}")
- end
-
- def group_invite_accepted_email(group_member_id)
- @group_member = GroupMember.find group_member_id
- return if @group_member.created_by.nil?
-
- @group = @group_member.group
-
- @target_url = group_url(@group)
- @current_user = @group_member.created_by
-
- mail(to: @group_member.created_by.notification_email,
- subject: subject("Invitation accepted"))
- end
-
- def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
- return if created_by_id.nil?
-
- @group = Group.find(group_id)
- @current_user = @created_by = User.find(created_by_id)
- @access_level = access_level
- @invite_email = invite_email
-
- @target_url = group_url(@group)
- mail(to: @created_by.notification_email,
- subject: subject("Invitation declined"))
- end
- end
-end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
new file mode 100644
index 00000000000..45311690293
--- /dev/null
+++ b/app/mailers/emails/members.rb
@@ -0,0 +1,86 @@
+module Emails
+ module Members
+ extend ActiveSupport::Concern
+ include MembersHelper
+
+ included do
+ helper_method :member_source, :member
+ end
+
+ def member_access_requested_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ # A project in a group can have no explicit owners/masters, in that case
+ # we fallbacks to the group's owners/masters.
+ if admins.empty? && member_source.respond_to?(:group) && member_source.group
+ admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ end
+
+ mail(to: admins,
+ subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+ end
+
+ def member_access_granted_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ mail(to: member.user.notification_email,
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
+ end
+
+ def member_access_denied_email(member_source_type, source_id, user_id)
+ @member_source_type = member_source_type
+ @member_source = member_source_class.find(source_id)
+ requester = User.find(user_id)
+
+ mail(to: requester.notification_email,
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
+ end
+
+ def member_invited_email(member_source_type, member_id, token)
+ @member_source_type = member_source_type
+ @member_id = member_id
+ @token = token
+
+ mail(to: member.invite_email,
+ subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+ end
+
+ def member_invite_accepted_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+ return unless member.created_by
+
+ mail(to: member.created_by.notification_email,
+ subject: subject('Invitation accepted'))
+ end
+
+ def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
+ return unless created_by_id
+
+ @member_source_type = member_source_type
+ @member_source = member_source_class.find(source_id)
+ @invite_email = invite_email
+ inviter = User.find(created_by_id)
+
+ mail(to: inviter.notification_email,
+ subject: subject('Invitation declined'))
+ end
+
+ def member
+ @member ||= Member.find(@member_id)
+ end
+
+ def member_source
+ @member_source ||= member.source
+ end
+
+ private
+
+ def member_source_class
+ @member_source_type.classify.constantize
+ end
+ end
+end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 377c2999d6c..236b6ab00d8 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,55 +1,5 @@
module Emails
module Projects
- def project_access_granted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.user
-
- mail(to: @project_member.user.notification_email,
- subject: subject("Access to project was granted"))
- end
-
- def project_member_invited_email(project_member_id, token)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
- @token = token
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.user
-
- mail(to: @project_member.invite_email,
- subject: "Invitation to join project #{@project.name_with_namespace}")
- end
-
- def project_invite_accepted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- return if @project_member.created_by.nil?
-
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.created_by
-
- mail(to: @project_member.created_by.notification_email,
- subject: subject("Invitation accepted"))
- end
-
- def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
- return if created_by_id.nil?
-
- @project = Project.find(project_id)
- @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,
- subject: subject("Invitation declined"))
- end
-
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
@@ -59,20 +9,32 @@ module Emails
subject: subject("Project was moved"))
end
- def repository_push_email(project_id, recipient, opts = {})
+ def project_was_exported_email(current_user, project)
+ @project = project
+ mail(to: current_user.notification_email,
+ subject: subject("Project was exported"))
+ end
+
+ def project_was_not_exported_email(current_user, project, errors)
+ @project = project
+ @errors = errors
+ mail(to: current_user.notification_email,
+ subject: subject("Project export error"))
+ end
+
+ def repository_push_email(project_id, opts = {})
@message =
- Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts)
+ Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
# used in notify layout
@target_url = @message.target_url
- @project = Project.find project_id
-
+ @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,
- to: @message.recipient,
subject: @message.subject)
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 826e5f96fa1..0cc709f68e4 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,11 +6,15 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Groups
include Emails::Builds
+ include Emails::Members
add_template_helper MergeRequestsHelper
+ add_template_helper DiffHelper
+ add_template_helper BlobHelper
add_template_helper EmailsHelper
+ add_template_helper MembersHelper
+ add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 6103a2947e2..eeb0ceba081 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,5 +1,6 @@
class Ability
class << self
+ # rubocop: disable Metrics/CyclomaticComplexity
def allowed(user, subject)
return anonymous_abilities(user, subject) if user.nil?
return [] unless user.is_a?(User)
@@ -9,7 +10,6 @@ class Ability
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)
@@ -19,24 +19,47 @@ class Ability
when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject)
when User then user_abilities
+ when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
+ when Ci::Runner then runner_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
+ # Given a list of users and a project this method returns the users that can
+ # read the given project.
+ def users_that_can_read_project(users, project)
+ if project.public?
+ users
+ else
+ users.select do |user|
+ if user.admin?
+ true
+ elsif project.internal? && !user.external?
+ true
+ elsif project.owner == user
+ true
+ elsif project.team.members.include?(user)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+
# List of possible abilities for anonymous user
def anonymous_abilities(user, subject)
- case true
- when subject.is_a?(PersonalSnippet)
+ if subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
- when subject.is_a?(ProjectSnippet)
+ elsif subject.is_a?(ProjectSnippet)
anonymous_project_snippet_abilities(subject)
- when subject.is_a?(CommitStatus)
+ elsif subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject)
- when subject.is_a?(Project) || subject.respond_to?(:project)
+ elsif subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
- when subject.is_a?(Group) || subject.respond_to?(:group)
+ elsif subject.is_a?(Group) || subject.respond_to?(:group)
anonymous_group_abilities(subject)
- when subject.is_a?(User)
+ elsif subject.is_a?(User)
anonymous_user_abilities
else
[]
@@ -60,7 +83,9 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
+ :read_pipeline,
:read_commit_status,
+ :read_container_image,
:download_code
]
@@ -132,10 +157,11 @@ class Ability
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
- if project.owner == user ||
- (project.group && project.group.has_owner?(user)) ||
- user.admin?
+ owner = user.admin? ||
+ project.owner == user ||
+ (project.group && project.group.has_owner?(user))
+ if owner
rules.push(*project_owner_rules)
end
@@ -144,6 +170,10 @@ class Ability
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
+
+ unless owner || project.team.member?(user) || project_group_member?(project, user)
+ rules << :request_access
+ end
end
if project.archived?
@@ -164,6 +194,8 @@ class Ability
project_report_rules
elsif team.guest?(user)
project_guest_rules
+ else
+ []
end
end
@@ -171,7 +203,8 @@ class Ability
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project,
- :read_commit_status
+ :read_commit_status,
+ :read_pipeline
]
end
@@ -203,6 +236,10 @@ class Ability
:admin_label,
:read_commit_status,
:read_build,
+ :read_container_image,
+ :read_pipeline,
+ :read_environment,
+ :read_deployment
]
end
@@ -214,9 +251,15 @@ class Ability
:update_commit_status,
:create_build,
:update_build,
+ :create_pipeline,
+ :update_pipeline,
:create_merge_request,
:create_wiki,
- :push_code
+ :push_code,
+ :create_container_image,
+ :update_container_image,
+ :create_environment,
+ :create_deployment
]
end
@@ -234,6 +277,8 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
+ :update_environment,
+ :update_deployment,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
@@ -242,7 +287,11 @@ class Ability
:admin_wiki,
:admin_project,
:admin_commit_status,
- :admin_build
+ :admin_build,
+ :admin_container_image,
+ :admin_pipeline,
+ :admin_environment,
+ :admin_deployment
]
end
@@ -285,6 +334,13 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
+ rules += named_abilities('pipeline')
+ rules += named_abilities('environment')
+ rules += named_abilities('deployment')
+ end
+
+ unless project.container_registry_enabled
+ rules += named_abilities('container_image')
end
rules
@@ -294,8 +350,11 @@ class Ability
rules = []
rules << :read_group if can_read_group?(user, group)
+ owner = user.admin? || group.has_owner?(user)
+ master = owner || group.has_master?(user)
+
# Only group masters and group owners can create new projects
- if group.has_master?(user) || group.has_owner?(user) || user.admin?
+ if master
rules += [
:create_projects,
:admin_milestones
@@ -303,7 +362,7 @@ class Ability
end
# Only group owner and administrators can admin group
- if group.has_owner?(user) || user.admin?
+ if owner
rules += [
:admin_group,
:admin_namespace,
@@ -312,6 +371,10 @@ class Ability
]
end
+ if group.public? || (group.internal? && !user.external?)
+ rules << :request_access unless group.users.include?(user)
+ end
+
rules.flatten
end
@@ -463,6 +526,18 @@ class Ability
rules
end
+ def runner_abilities(user, runner)
+ if user.is_admin?
+ [:assign_runner]
+ elsif runner.is_shared? || runner.locked?
+ []
+ elsif user.ci_authorized_runners.include?(runner)
+ [:assign_runner]
+ else
+ []
+ end
+ end
+
def user_abilities
[:read_user]
end
@@ -475,10 +550,6 @@ class Ability
end
end
- def external_issue_abilities(user, subject)
- project_abilities(user, subject.project)
- end
-
private
def restricted_public_level?
@@ -497,7 +568,7 @@ class Ability
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)
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
@@ -505,5 +576,13 @@ class Ability
rules
end
+
+ def project_group_member?(project, user)
+ project.group &&
+ (
+ project.group.members.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
+ end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 7039db2d41e..c6f77cc055f 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,11 +1,13 @@
class ApplicationSetting < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :runners_registration_token
+ add_authentication_token_field :health_check_access_token
CACHE_KEY = 'application_setting.last'
serialize :restricted_visibility_levels
serialize :import_sources
+ serialize :disabled_oauth_sign_in_sources, Array
serialize :restricted_signup_domains, Array
attr_accessor :restricted_signup_domains_raw
@@ -49,6 +51,17 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :container_registry_token_expire_delay,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :repository_storage,
+ presence: true,
+ inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
+
+ validates :enabled_git_access_protocol,
+ inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@@ -69,7 +82,18 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
+ unless value.nil?
+ value.each do |source|
+ unless Devise.omniauth_providers.include?(source.to_sym)
+ record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
+ end
+ end
+ end
+ end
+
before_save :ensure_runners_registration_token
+ before_save :ensure_health_check_access_token
after_commit do
Rails.cache.write(CACHE_KEY, self)
@@ -85,6 +109,10 @@ class ApplicationSetting < ActiveRecord::Base
Rails.cache.delete(CACHE_KEY)
end
+ def self.cached
+ Rails.cache.fetch(CACHE_KEY)
+ end
+
def self.create_from_defaults
create(
default_projects_limit: Settings.gitlab['default_projects_limit'],
@@ -92,14 +120,17 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
- sign_in_text: Settings.extra['sign_in_text'],
+ sign_in_text: nil,
+ after_sign_up_text: nil,
+ help_page_text: nil,
+ shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
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'],
+ import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
@@ -107,6 +138,11 @@ class ApplicationSetting < ActiveRecord::Base
recaptcha_enabled: false,
akismet_enabled: false,
repository_checks_enabled: true,
+ disabled_oauth_sign_in_sources: [],
+ send_user_confirmation_email: false,
+ container_registry_token_expire_delay: 5,
+ repository_storage: 'default',
+ user_default_external: false,
)
end
@@ -133,4 +169,8 @@ class ApplicationSetting < ActiveRecord::Base
def runners_registration_token
ensure_runners_registration_token!
end
+
+ def health_check_access_token
+ ensure_health_check_access_token!
+ end
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
new file mode 100644
index 00000000000..46b17479d6d
--- /dev/null
+++ b/app/models/award_emoji.rb
@@ -0,0 +1,26 @@
+class AwardEmoji < ActiveRecord::Base
+ DOWNVOTE_NAME = "thumbsdown".freeze
+ UPVOTE_NAME = "thumbsup".freeze
+
+ include Participable
+
+ belongs_to :awardable, polymorphic: true
+ belongs_to :user
+
+ validates :awardable, :user, presence: true
+ validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names }
+ validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
+
+ participant :user
+
+ scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
+ scope :upvotes, -> { where(name: UPVOTE_NAME) }
+
+ def downvote?
+ self.name == DOWNVOTE_NAME
+ end
+
+ def upvote?
+ self.name == UPVOTE_NAME
+ end
+end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 0fea6b7f576..4279ea2ce57 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -24,7 +24,7 @@ class Blob < SimpleDelegator
end
def only_display_raw?
- size && size > 5.megabytes
+ size && truncated?
end
def svg?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index c9506dd3057..e189dbac285 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,21 +11,21 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
+ scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
+ scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
+ scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
+ before_save :update_artifacts_size, if: :artifacts_file_changed?
before_destroy { project }
after_create :execute_hooks
class << self
- def last_month
- where('created_at > ?', Date.today - 1.month)
- end
-
def first_pending
pending.unstarted.order('created_at ASC').first
end
@@ -38,21 +38,23 @@ module Ci
new_build.save
end
- def retry(build)
+ def retry(build, user = nil)
new_build = Ci::Build.new(status: 'pending')
new_build.ref = build.ref
new_build.tag = build.tag
new_build.options = build.options
new_build.commands = build.commands
new_build.tag_list = build.tag_list
- new_build.gl_project_id = build.gl_project_id
- new_build.commit_id = build.commit_id
+ new_build.project = build.project
+ new_build.pipeline = build.pipeline
new_build.name = build.name
new_build.allow_failure = build.allow_failure
new_build.stage = build.stage
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
+ new_build.user = user
new_build.save
+ MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
end
end
@@ -65,38 +67,49 @@ module Ci
# 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
+ build.pipeline.create_next_builds(build) if build.pipeline
end
after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
build.execute_hooks
end
+
+ after_transition any => [:success] do |build|
+ if build.environment.present?
+ service = CreateDeploymentService.new(build.project, build.user,
+ environment: build.environment,
+ sha: build.sha,
+ ref: build.ref,
+ tag: build.tag)
+ service.execute(build)
+ end
+ end
end
def retryable?
- project.builds_enabled? && commands.present?
+ project.builds_enabled? && commands.present? && complete?
end
def retried?
- !self.commit.statuses.latest.include?(self)
- end
-
- def retry
- Ci::Build.retry(self)
+ !self.pipeline.statuses.latest.include?(self)
end
def depends_on_builds
# Get builds of the same type
- latest_builds = self.commit.builds.latest
+ latest_builds = self.pipeline.builds.latest
# Return builds from previous stages
latest_builds.where('stage_idx < ?', stage_idx)
end
def trace_html
- html = Ci::Ansi2html::convert(trace) if trace.present?
- html || ''
+ trace_with_state[:html] || ''
+ end
+
+ def trace_with_state(state = nil)
+ trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
+ trace_with_state || {}
end
def timeout
@@ -109,16 +122,16 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
- .where(source_branch: ref, source_project_id: commit.gl_project_id)
+ .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
- merge_request.commits.any? { |ci| ci.id == commit.sha }
+ merge_request.commits.any? { |ci| ci.id == pipeline.sha }
end
end
def project_id
- commit.project.id
+ pipeline.project_id
end
def project_name
@@ -189,7 +202,7 @@ module Ci
def trace_length
if raw_trace
- raw_trace.length
+ raw_trace.bytesize
else
0
end
@@ -201,7 +214,7 @@ module Ci
end
def recreate_trace_dir
- unless Dir.exists?(dir_to_trace)
+ unless Dir.exist?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace)
end
end
@@ -211,7 +224,7 @@ module Ci
recreate_trace_dir
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
- File.open(path_to_trace, 'a') do |f|
+ File.open(path_to_trace, 'ab') do |f|
f.write(trace_part)
end
end
@@ -281,16 +294,16 @@ module Ci
project.runners_token
end
- def valid_token? token
+ def valid_token?(token)
project.valid_runners_token? token
end
- def can_be_served?(runner)
- (tag_list - runner.tag_list).empty?
+ def has_tags?
+ tag_list.any?
end
def any_runners_online?
- project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
+ project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end
def stuck?
@@ -302,10 +315,11 @@ module Ci
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
+ project.running_or_pending_build_count(force: true)
end
def artifacts?
- artifacts_file.exists?
+ !artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
@@ -313,14 +327,24 @@ module Ci
end
def artifacts_metadata_entry(path, **options)
- Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
+ metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
+ artifacts_metadata.path,
+ path,
+ **options)
+
+ metadata.to_entry
+ end
+
+ def erase_artifacts!
+ remove_artifacts_file!
+ remove_artifacts_metadata!
+ save
end
def erase(opts = {})
return false unless erasable?
- remove_artifacts_file!
- remove_artifacts_metadata!
+ erase_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
@@ -333,14 +357,41 @@ module Ci
!self.erased_at.nil?
end
+ def artifacts_expired?
+ artifacts_expire_at && artifacts_expire_at < Time.now
+ end
+
+ def artifacts_expire_in
+ artifacts_expire_at - Time.now if artifacts_expire_at
+ end
+
+ def artifacts_expire_in=(value)
+ self.artifacts_expire_at =
+ if value
+ Time.now + ChronicDuration.parse(value)
+ end
+ end
+
+ def keep_artifacts!
+ self.update(artifacts_expire_at: nil)
+ end
+
private
+ def update_artifacts_size
+ self.artifacts_size = if artifacts_file.exists?
+ artifacts_file.size
+ else
+ nil
+ end
+ end
+
def erase_trace!
self.trace = nil
end
def update_erased!(user = nil)
- self.update(erased_by: user, erased_at: Time.now)
+ self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
def yaml_variables
@@ -348,8 +399,8 @@ module Ci
end
def global_yaml_variables
- if commit.config_processor
- commit.config_processor.global_variables.map do |key, value|
+ if pipeline.config_processor
+ pipeline.config_processor.global_variables.map do |key, value|
{ key: key, value: value, public: true }
end
else
@@ -358,8 +409,8 @@ module Ci
end
def job_yaml_variables
- if commit.config_processor
- commit.config_processor.job_variables(name).map do |key, value|
+ if pipeline.config_processor
+ pipeline.config_processor.job_variables(name).map do |key, value|
{ key: key, value: value, public: true }
end
else
diff --git a/app/models/ci/commit.rb b/app/models/ci/pipeline.rb
index f4b61c75ab6..fa4071e2482 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,14 +1,14 @@
module Ci
- class Commit < ActiveRecord::Base
+ class Pipeline < ActiveRecord::Base
extend Ci::Model
include Statuseable
- belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- has_many :statuses, class_name: 'CommitStatus'
- has_many :builds, class_name: 'Ci::Build'
- has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+ self.table_name = 'ci_commits'
- delegate :stages, to: :statuses
+ belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
+ has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha
validates_presence_of :status
@@ -16,13 +16,15 @@ module Ci
# Invalidate object and save if when touched
after_touch :update_state
+ after_save :keep_around_commits
def self.truncate_sha(sha)
sha[0...8]
end
def self.stages
- CommitStatus.where(commit: all).stages
+ # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
+ CommitStatus.where(pipeline: pluck(:id)).stages
end
def project_id
@@ -36,22 +38,22 @@ module Ci
end
def git_author_name
- commit_data.author_name if commit_data
+ commit.try(:author_name)
end
def git_author_email
- commit_data.author_email if commit_data
+ commit.try(:author_email)
end
def git_commit_message
- commit_data.message if commit_data
+ commit.try(:message)
end
def short_sha
- Ci::Commit.truncate_sha(sha)
+ Ci::Pipeline.truncate_sha(sha)
end
- def commit_data
+ def commit
@commit ||= project.commit(sha)
rescue
nil
@@ -67,11 +69,39 @@ module Ci
end
end
+ def cancelable?
+ builds.running_or_pending.any?
+ end
+
+ def cancel_running
+ builds.running_or_pending.each(&:cancel)
+ end
+
+ def retry_failed(user)
+ builds.latest.failed.select(&:retryable?).each do |build|
+ Ci::Build.retry(build, user)
+ end
+ end
+
+ def latest?
+ return false unless ref
+ commit = project.commit(ref)
+ return false unless commit
+ commit.sha == sha
+ end
+
+ def triggered?
+ trigger_requests.any?
+ end
+
def create_builds(user, trigger_request = nil)
+ ##
+ # We persist pipeline only if there are builds available
+ #
return unless config_processor
- config_processor.stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
- end
+
+ build_builds_for_stages(config_processor.stages, user,
+ 'success', trigger_request) && save
end
def create_next_builds(build)
@@ -89,10 +119,10 @@ module Ci
prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status
- # create builds for next stages based
- next_stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
- end
+ # build builds for next stage that has builds available
+ # and save pipeline if we have builds
+ build_builds_for_stages(next_stages, build.user, prior_status,
+ build.trigger_request) && save
end
def retried
@@ -113,10 +143,10 @@ module Ci
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- save_yaml_error(e.message)
+ self.yaml_errors = e.message
nil
rescue
- save_yaml_error("Undefined error")
+ self.yaml_errors = 'Undefined error'
nil
end
end
@@ -134,11 +164,43 @@ module Ci
end
def skip_ci?
- git_commit_message =~ /(\[ci skip\])/ if git_commit_message
+ git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
+ end
+
+ def environments
+ builds.where.not(environment: nil).success.pluck(:environment).uniq
+ end
+
+ # Manually set the notes for a Ci::Pipeline
+ # There is no ActiveRecord relation between Ci::Pipeline and notes
+ # as they are related to a commit sha. This method helps importing
+ # them using the +Gitlab::ImportExport::RelationFactory+ class.
+ def notes=(notes)
+ notes.each do |note|
+ note[:id] = nil
+ note[:commit_id] = sha
+ note[:noteable_id] = self['id']
+ note.save!
+ end
+ end
+
+ def notes
+ Note.for_commit_id(sha)
end
private
+ def build_builds_for_stages(stages, user, status, trigger_request)
+ ##
+ # Note that `Array#any?` implements a short circuit evaluation, so we
+ # build builds only for the first stage that has builds available.
+ #
+ stages.any? do |stage|
+ CreateBuildsService.new(self)
+ .execute(stage, user, status, trigger_request).present?
+ end
+ end
+
def update_state
statuses.reload
self.status = if yaml_errors.blank?
@@ -152,10 +214,9 @@ module Ci
save
end
- def save_yaml_error(error)
- return if self.yaml_errors?
- self.yaml_errors = error
- update_state
+ def keep_around_commits
+ project.repository.keep_around(self.sha)
+ project.repository.keep_around(self.before_sha)
end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 0cdb64d6c33..b64ec79ec2b 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -3,7 +3,8 @@ module Ci
extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago
- AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
+ AVAILABLE_SCOPES = %w[specific shared active paused online]
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
@@ -25,6 +26,15 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
+ scope :assignable_for, ->(project) do
+ # FIXME: That `to_sql` is needed to workaround a weird Rails bug.
+ # Without that, placeholders would miss one and couldn't match.
+ where(locked: false).
+ where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ end
+
+ validate :tag_constraints
+
acts_as_taggable
# Searches for runners matching the given query.
@@ -53,11 +63,11 @@ module Ci
def assign_to(project, current_user = nil)
self.is_shared = false if shared?
self.save
- project.runner_projects.create!(runner_id: self.id)
+ project.runner_projects.create(runner_id: self.id)
end
def display_name
- return short_sha unless !description.blank?
+ return short_sha if description.blank?
description
end
@@ -88,6 +98,10 @@ module Ci
!shared?
end
+ def can_pick?(build)
+ assignable_for?(build.project) && accepting_tags?(build)
+ end
+
def only_for?(project)
projects == [project]
end
@@ -95,5 +109,26 @@ module Ci
def short_sha
token[0...8] if token
end
+
+ def has_tags?
+ tag_list.any?
+ end
+
+ private
+
+ def tag_constraints
+ unless has_tags? || run_untagged?
+ errors.add(:tags_list,
+ 'can not be empty when runner is not allowed to pick untagged jobs')
+ end
+ end
+
+ def assignable_for?(project)
+ !locked? || projects.exists?(id: project.id)
+ end
+
+ def accepting_tags?(build)
+ (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
+ end
end
end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 872d5fb31de..fcf2b6dc5e2 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,9 +1,9 @@
module Ci
class TriggerRequest < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :trigger, class_name: 'Ci::Trigger'
- belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build'
serialize :variables
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 10802f64813..c9c47ec7419 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -11,6 +11,10 @@ module Ci
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
+ attr_encrypted :value,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 562c3ed15b2..2ef3973c160 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -8,7 +8,10 @@ class Commit
include StaticModel
attr_mentionable :safe_message, pipeline: :single_line
- participant :author, :committer, :notes
+
+ participant :author
+ participant :committer
+ participant :notes_with_associations
attr_accessor :project
@@ -194,6 +197,10 @@ class Commit
project.notes.for_commit_id(self.id)
end
+ def notes_with_associations
+ notes.includes(:author)
+ end
+
def method_missing(m, *args, &block)
@raw.send(m, *args, &block)
end
@@ -207,19 +214,26 @@ class Commit
@raw.short_id(7)
end
- def ci_commits
- @ci_commits ||= project.ci_commits.where(sha: sha)
+ def diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: self.parent_id || self.sha,
+ head_sha: self.sha
+ )
+ end
+
+ def pipelines
+ @pipeline ||= project.pipelines.where(sha: sha)
end
def status
return @status if defined?(@status)
- @status ||= ci_commits.status
+ @status ||= pipelines.status
end
def revert_branch_name
"revert-#{short_id}"
end
-
+
def cherry_pick_branch_name
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
@@ -251,17 +265,45 @@ class 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) }
+ ext = all_references(current_user)
+
+ noteable.notes_with_associations.system.each do |note|
+ note.all_references(current_user, extractor: ext)
+ end
+
+ ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end
def change_type_title
merged_merge_request ? 'merge request' : 'commit'
end
+ # Get the URI type of the given path
+ #
+ # Used to build URLs to files in the repository in GFM.
+ #
+ # path - String path to check
+ #
+ # Examples:
+ #
+ # uri_type('doc/README.md') # => :blob
+ # uri_type('doc/logo.png') # => :raw
+ # uri_type('doc/api') # => :tree
+ # uri_type('not/found') # => :nil
+ #
+ # Returns a symbol
+ def uri_type(path)
+ entry = @raw.tree.path(path)
+ if entry[:type] == :blob
+ blob = Gitlab::Git::Blob.new(name: entry[:name])
+ blob.image? ? :raw : :blob
+ else
+ entry[:type]
+ end
+ rescue Rugged::TreeError
+ nil
+ end
+
private
def repo_changes
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 51673897d98..630ee9601e0 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -23,7 +23,7 @@ class CommitRange
attr_reader :commit_from, :notation, :commit_to
attr_reader :ref_from, :ref_to
- # Optional Project model
+ # The Project model
attr_accessor :project
# The beginning and ending refs can be named or SHAs, and
@@ -56,13 +56,13 @@ class CommitRange
# Initialize a CommitRange
#
# range_string - The String commit range.
- # project - An optional Project model.
+ # project - The Project model.
#
# Raises ArgumentError if `range_string` does not match `PATTERN`.
def initialize(range_string, project)
@project = project
- range_string.strip!
+ range_string = range_string.strip
unless range_string =~ /\A#{PATTERN}\z/
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cacbc13b391..e437e3417a8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,20 +1,24 @@
class CommitStatus < ActiveRecord::Base
include Statuseable
+ include Importable
self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- belongs_to :commit, class_name: 'Ci::Commit', touch: true
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
- validates :commit, presence: true
+ delegate :commit, to: :pipeline
+
+ validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
- scope :ordered, -> { order(:ref, :stage_idx, :name) }
+ scope :retried, -> { where.not(id: latest) }
+ scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do
@@ -43,24 +47,30 @@ class CommitStatus < ActiveRecord::Base
end
after_transition [:pending, :running] => :success do |commit_status|
- MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
+ MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
+ end
+
+ after_transition any => :failed do |commit_status|
+ MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
end
end
- delegate :sha, :short_sha, to: :commit
+ delegate :sha, :short_sha, to: :pipeline
def before_sha
- commit.before_sha || Gitlab::Git::BLANK_SHA
+ pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
def self.stages
- order_by = 'max(stage_idx)'
- group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
+ # We group by stage name, but order stages by theirs' index
+ unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end
def self.stages_status
- all.stages.inject({}) do |h, stage|
- h[stage] = all.where(stage: stage).status
+ # We execute subquery for each stage to calculate a stage status
+ statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
+ statuses.inject({}) do |h, k|
+ h[k.first] = k.last
h
end
end
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
new file mode 100644
index 00000000000..eedd32a729f
--- /dev/null
+++ b/app/models/concerns/access_requestable.rb
@@ -0,0 +1,16 @@
+# == AccessRequestable concern
+#
+# Contains functionality related to objects that can receive request for access.
+#
+# Used by Project, and Group.
+#
+module AccessRequestable
+ extend ActiveSupport::Concern
+
+ def request_access(user)
+ members.create(
+ access_level: Gitlab::Access::DEVELOPER,
+ user: user,
+ requested_at: Time.now.utc)
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
new file mode 100644
index 00000000000..06beff177b1
--- /dev/null
+++ b/app/models/concerns/awardable.rb
@@ -0,0 +1,83 @@
+module Awardable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy
+
+ if self < Participable
+ # By default we always load award_emoji user association
+ participant :award_emoji
+ end
+ end
+
+ module ClassMethods
+ def order_upvotes_desc
+ order_votes_desc(AwardEmoji::UPVOTE_NAME)
+ end
+
+ def order_downvotes_desc
+ order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
+ end
+
+ def order_votes_desc(emoji_name)
+ awardable_table = self.arel_table
+ awards_table = AwardEmoji.arel_table
+
+ join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
+ awards_table[:awardable_id].eq(awardable_table[:id]).and(
+ awards_table[:awardable_type].eq(self.name).and(
+ awards_table[:name].eq(emoji_name)
+ )
+ )
+ ).join_sources
+
+ joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
+ end
+ end
+
+ def grouped_awards(with_thumbs: true)
+ # By default we always load award_emoji user association
+ awards = award_emoji.group_by(&:name)
+
+ if with_thumbs
+ awards[AwardEmoji::UPVOTE_NAME] ||= []
+ awards[AwardEmoji::DOWNVOTE_NAME] ||= []
+ end
+
+ awards
+ end
+
+ def downvotes
+ award_emoji.downvotes.count
+ end
+
+ def upvotes
+ award_emoji.upvotes.count
+ end
+
+ def emoji_awardable?
+ true
+ end
+
+ def awarded_emoji?(emoji_name, current_user)
+ award_emoji.where(name: emoji_name, user: current_user).exists?
+ end
+
+ def create_award_emoji(name, current_user)
+ return unless emoji_awardable?
+
+ award_emoji.create(name: name, user: current_user)
+ end
+
+ def remove_award_emoji(name, current_user)
+ award_emoji.where(name: name, user: current_user).destroy_all
+ end
+
+ def toggle_award_emoji(emoji_name, current_user)
+ if awarded_emoji?(emoji_name, current_user)
+ remove_award_emoji(emoji_name, current_user)
+ else
+ create_award_emoji(emoji_name, current_user)
+ end
+ end
+end
diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb
new file mode 100644
index 00000000000..019ef755849
--- /dev/null
+++ b/app/models/concerns/importable.rb
@@ -0,0 +1,6 @@
+module Importable
+ extend ActiveSupport::Concern
+
+ attr_accessor :importing
+ alias_method :importing?, :importing
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index c1248b53031..acb6f5a2998 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -10,13 +10,24 @@ module Issuable
include Mentionable
include Subscribable
include StripAttribute
+ include Awardable
included do
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone
- has_many :notes, as: :noteable, dependent: :destroy
+ has_many :notes, as: :noteable, dependent: :destroy do
+ def authors_loaded?
+ # We check first if we're loaded to not load unnecessarily.
+ loaded? && to_a.all? { |note| note.association(:author).loaded? }
+ end
+
+ def award_emojis_loaded?
+ # We check first if we're loaded to not load unnecessarily.
+ loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
+ end
+ end
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
@@ -31,18 +42,21 @@ module Issuable
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
+ scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
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, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date DESC, milestones.id DESC') }
- scope :order_milestone_due_asc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date ASC, milestones.id ASC') }
- 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 :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
+ scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
+ scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
+
+ 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 :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
- scope :outer_join_milestone, -> { joins("LEFT OUTER JOIN milestones ON milestones.id = #{table_name}.milestone_id") }
delegate :name,
:email,
@@ -56,11 +70,23 @@ module Issuable
prefix: true
attr_mentionable :title, pipeline: :single_line
- attr_mentionable :description, cache: true
- participant :author, :assignee, :notes_with_associations
+ attr_mentionable :description
+
+ participant :author
+ participant :assignee
+ participant :notes_with_associations
+
strip_attributes :title
acts_as_paranoid
+
+ after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+
+ def update_assignee_cache_counts
+ # make sure we flush the cache for both the old *and* new assignee
+ User.find(assignee_id_was).update_cache_counts if assignee_id_was
+ assignee.update_cache_counts if assignee
+ end
end
module ClassMethods
@@ -89,46 +115,63 @@ module Issuable
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
+ def sort(method, excluded_labels: [])
+ sorted = 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
+ when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
+ else
+ order_by(method)
+ end
+
+ # Break ties with the ID column for pagination
+ sorted.order(id: :desc)
+ end
+
+ def order_labels_priority(excluded_labels: [])
+ select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+ group(arel_table[:id]).
+ reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ end
+
+ def with_label(title, sort = nil)
+ if title.is_a?(Array) && title.size > 1
+ joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
else
- order_by(method)
+ joins(:labels).where(labels: { title: title })
end
end
- def order_downvotes_desc
- order_votes_desc('thumbsdown')
- end
+ # Includes table keys in group by clause when sorting
+ # preventing errors in postgres
+ #
+ # Returns an array of arel columns
+ def grouping_columns(sort)
+ grouping_columns = [arel_table[:id]]
+
+ if ["milestone_due_desc", "milestone_due_asc"].include?(sort)
+ milestone_table = Milestone.arel_table
+ grouping_columns << milestone_table[:id]
+ grouping_columns << milestone_table[:due_date]
+ end
- def order_upvotes_desc
- order_votes_desc('thumbsup')
+ grouping_columns
end
- def order_votes_desc(award_emoji_name)
- issuable_table = self.arel_table
- note_table = Note.arel_table
+ private
- 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
+ def highest_label_priority(excluded_labels)
+ query = Label.select(Label.arel_table[:priority].minimum).
+ joins(:label_links).
+ where(label_links: { target_type: name }).
+ where("label_links.target_id = #{table_name}.id").
+ reorder(nil)
- joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
- end
+ query.where.not(title: excluded_labels) if excluded_labels.present?
- def with_label(title)
- if title.is_a?(Array) && title.size > 1
- joins(:labels).where(labels: { title: title }).group(arel_table[:id]).having("COUNT(DISTINCT labels.title) = #{title.size}")
- else
- joins(:labels).where(labels: { title: title })
- end
+ query
end
end
@@ -140,10 +183,6 @@ module Issuable
today? && created_at == updated_at
end
- def is_assigned?
- !!assignee_id
- end
-
def is_being_reassigned?
assignee_id_changed?
end
@@ -152,16 +191,14 @@ module Issuable
opened? || reopened?
end
- def downvotes
- notes.awards.where(note: "thumbsdown").count
- end
-
- def upvotes
- notes.awards.where(note: "thumbsup").count
- end
-
def user_notes_count
- notes.user.count
+ if notes.loaded?
+ # Use the in-memory association to select and count to avoid hitting the db
+ notes.to_a.count { |note| !note.system? }
+ else
+ # do the count query
+ notes.user.count
+ end
end
def subscribed_without_subscriptions?(user)
@@ -182,6 +219,10 @@ module Issuable
hook_data
end
+ def labels_array
+ labels.to_a
+ end
+
def label_names
labels.order('title ASC').pluck(:title)
end
@@ -217,7 +258,20 @@ module Issuable
end
def notes_with_associations
- notes.includes(:author, :project)
+ # If A has_many Bs, and B has_many Cs, and you do
+ # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
+ # will do the inclusion again. So, we check if all notes in the relation
+ # already have their authors loaded (possibly because the scope
+ # `inc_notes_with_associations` was used) and skip the inclusion if that's
+ # the case.
+ includes = []
+ includes << :author unless notes.authors_loaded?
+ includes << :award_emoji unless notes.award_emojis_loaded?
+ if includes.any?
+ notes.includes(includes)
+ else
+ notes
+ end
end
def updated_tasks
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index b381d225485..8cac47246db 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -23,7 +23,7 @@ module Mentionable
included do
if self < Participable
- participant ->(current_user) { mentioned_users(current_user) }
+ participant -> (user, ext) { all_references(user, extractor: ext) }
end
end
@@ -43,23 +43,22 @@ module Mentionable
self
end
- def all_references(current_user = nil, text = nil)
- ext = Gitlab::ReferenceExtractor.new(self.project, current_user || self.author, self.author)
+ def all_references(current_user = nil, text = nil, extractor: nil)
+ extractor ||= Gitlab::ReferenceExtractor.
+ new(project, current_user)
if text
- ext.analyze(text)
+ extractor.analyze(text, author: author)
else
self.class.mentionable_attrs.each do |attr, options|
- text = send(attr)
+ text = __send__(attr)
+ options = options.merge(cache_key: [self, attr], author: author)
- context = options.dup
- context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
-
- ext.analyze(text, context)
+ extractor.analyze(text, options)
end
end
- ext
+ extractor
end
def mentioned_users(current_user = nil)
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
new file mode 100644
index 00000000000..2785fbb21c9
--- /dev/null
+++ b/app/models/concerns/note_on_diff.rb
@@ -0,0 +1,52 @@
+module NoteOnDiff
+ extend ActiveSupport::Concern
+
+ NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+
+ included do
+ delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
+ end
+
+ def diff_note?
+ true
+ end
+
+ def diff_file
+ raise NotImplementedError
+ end
+
+ def diff_line
+ raise NotImplementedError
+ end
+
+ def for_line?(line)
+ raise NotImplementedError
+ end
+
+ def diff_attributes
+ raise NotImplementedError
+ end
+
+ def can_be_award_emoji?
+ false
+ end
+
+ # Returns an array of at most 16 highlighted lines above a diff note
+ def truncated_diff_lines
+ prev_lines = []
+
+ highlighted_diff_lines.each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+
+ break if for_line?(line)
+
+ prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ prev_lines
+ end
+end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index fc6f83b918b..9822844357d 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -3,8 +3,6 @@
# Contains functionality related to objects that can have participants, such as
# an author, an assignee and people mentioned in its description or comments.
#
-# Used by Issue, Note, MergeRequest, Snippet and Commit.
-#
# Usage:
#
# class Issue < ActiveRecord::Base
@@ -12,22 +10,36 @@
#
# # ...
#
-# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) }
+# participant :author
+# participant :assignee
+# participant :notes
+#
+# participant -> (current_user, ext) do
+# ext.analyze('...')
+# end
# end
#
# issue = Issue.last
# users = issue.participants
-# # `users` will contain the issue's author, its assignee,
-# # all users returned by its #mentioned_users method,
-# # as well as all participants to all of the issue's notes,
-# # since Note implements Participable as well.
-#
module Participable
extend ActiveSupport::Concern
module ClassMethods
- def participant(*attrs)
- participant_attrs.concat(attrs)
+ # Adds a list of participant attributes. Attributes can either be symbols or
+ # Procs.
+ #
+ # When using a Proc instead of a Symbol the Proc will be given two
+ # arguments:
+ #
+ # 1. The current user (as an instance of User)
+ # 2. An instance of `Gitlab::ReferenceExtractor`
+ #
+ # It is expected that a Proc populates the given reference extractor
+ # instance with data. The return value of the Proc is ignored.
+ #
+ # attr - The name of the attribute or a Proc
+ def participant(attr)
+ participant_attrs << attr
end
def participant_attrs
@@ -35,42 +47,52 @@ module Participable
end
end
- # Be aware that this method makes a lot of sql queries.
- # Save result into variable if you are going to reuse it inside same request
- def participants(current_user = self.author)
- participants =
- Gitlab::ReferenceExtractor.lazily do
- self.class.participant_attrs.flat_map do |attr|
- value =
- if attr.respond_to?(:call)
- instance_exec(current_user, &attr)
- else
- send(attr)
- end
-
- participants_for(value, current_user)
- end.compact.uniq
- end
-
- unless Gitlab::ReferenceExtractor.lazy?
- participants.select! do |user|
- user.can?(:read_project, project)
- end
+ # Returns the users participating in a discussion.
+ #
+ # This method processes attributes of objects in breadth-first order.
+ #
+ # Returns an Array of User instances.
+ def participants(current_user = nil)
+ @participants ||= Hash.new do |hash, user|
+ hash[user] = raw_participants(user)
end
- participants
+ @participants[current_user]
end
private
- def participants_for(value, current_user = nil)
- case value
- when User, Banzai::LazyReference
- [value]
- when Enumerable, ActiveRecord::Relation
- value.flat_map { |v| participants_for(v, current_user) }
- when Participable
- value.participants(current_user)
+ def raw_participants(current_user = nil)
+ current_user ||= author
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ participants = Set.new
+ process = [self]
+
+ until process.empty?
+ source = process.pop
+
+ case source
+ when User
+ participants << source
+ when Participable
+ source.class.participant_attrs.each do |attr|
+ if attr.respond_to?(:call)
+ source.instance_exec(current_user, ext, &attr)
+ else
+ process << source.__send__(attr)
+ end
+ end
+ when Enumerable, ActiveRecord::Relation
+ # This uses reverse_each so we can use "pop" to get the next value to
+ # process (in order). Using unshift instead of pop would require
+ # moving all Array values one index to the left (which can be
+ # expensive).
+ source.reverse_each { |obj| process << obj }
+ end
end
+
+ participants.merge(ext.users)
+
+ Ability.users_that_can_read_project(participants.to_a, project)
end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index ce064f675ae..dee940a3f88 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -49,6 +49,10 @@ module Referable
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+ def reference_valid?(reference)
+ true
+ end
+
def link_reference_pattern(route, pattern)
%r{
(?<url>
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index d5a881b2445..083257f1005 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -36,6 +36,12 @@ module Subscribable
update(subscribed: !subscribed?(user))
end
+ def subscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: true)
+ end
+
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
new file mode 100644
index 00000000000..520026c18dd
--- /dev/null
+++ b/app/models/deployment.rb
@@ -0,0 +1,35 @@
+class Deployment < ActiveRecord::Base
+ include InternalId
+
+ belongs_to :project, required: true, validate: true
+ belongs_to :environment, required: true, validate: true
+ belongs_to :user
+ belongs_to :deployable, polymorphic: true
+
+ validates :sha, presence: true
+ validates :ref, presence: true
+
+ delegate :name, to: :environment, prefix: true
+
+ after_save :keep_around_commit
+
+ def commit
+ project.commit(sha)
+ end
+
+ def commit_title
+ commit.try(:title)
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
+ def last?
+ self == environment.last_deployment
+ end
+
+ def keep_around_commit
+ project.repository.keep_around(self.sha)
+ end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
new file mode 100644
index 00000000000..9671955db36
--- /dev/null
+++ b/app/models/diff_note.rb
@@ -0,0 +1,127 @@
+class DiffNote < Note
+ include NoteOnDiff
+
+ serialize :original_position, Gitlab::Diff::Position
+ serialize :position, Gitlab::Diff::Position
+
+ validates :original_position, presence: true
+ validates :position, presence: true
+ validates :diff_line, presence: true
+ validates :line_code, presence: true, line_code: true
+ validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+ validate :positions_complete
+ validate :verify_supported
+
+ before_validation :set_original_position, :update_position, on: :create
+ before_validation :set_line_code
+ after_save :keep_around_commits
+
+ class << self
+ def build_discussion_id(noteable_type, noteable_id, position)
+ [super(noteable_type, noteable_id), *position.key].join("-")
+ end
+ end
+
+ def new_diff_note?
+ true
+ end
+
+ def diff_attributes
+ { position: position.to_json }
+ end
+
+ def discussion_id
+ @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
+ end
+
+ def original_discussion_id
+ @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
+ end
+
+ def position=(new_position)
+ if new_position.is_a?(String)
+ new_position = JSON.parse(new_position) rescue nil
+ end
+
+ if new_position.is_a?(Hash)
+ new_position = new_position.with_indifferent_access
+ new_position = Gitlab::Diff::Position.new(new_position)
+ end
+
+ super(new_position)
+ end
+
+ def diff_file
+ @diff_file ||= self.original_position.diff_file(self.project.repository)
+ end
+
+ def diff_line
+ @diff_line ||= diff_file.line_for_position(self.original_position) if diff_file
+ end
+
+ def for_line?(line)
+ diff_file.position(line) == self.original_position
+ end
+
+ def active?(diff_refs = nil)
+ return false unless supported?
+ return true if for_commit?
+
+ diff_refs ||= self.noteable.diff_refs
+
+ self.position.diff_refs == diff_refs
+ end
+
+ private
+
+ def supported?
+ !self.for_merge_request? || self.noteable.support_new_diff_notes?
+ end
+
+ def set_original_position
+ self.original_position = self.position.dup
+ end
+
+ def set_line_code
+ self.line_code = self.position.line_code(self.project.repository)
+ end
+
+ def update_position
+ return unless supported?
+ return if for_commit?
+
+ return if active?
+
+ Notes::DiffPositionUpdateService.new(
+ self.project,
+ nil,
+ old_diff_refs: self.position.diff_refs,
+ new_diff_refs: self.noteable.diff_refs,
+ paths: self.position.paths
+ ).execute(self)
+ end
+
+ def verify_supported
+ return if supported?
+
+ errors.add(:noteable, "doesn't support new-style diff notes")
+ end
+
+ def positions_complete
+ return if self.original_position.complete? && self.position.complete?
+
+ errors.add(:position, "is invalid")
+ end
+
+ def keep_around_commits
+ project.repository.keep_around(self.original_position.base_sha)
+ project.repository.keep_around(self.original_position.start_sha)
+ project.repository.keep_around(self.original_position.head_sha)
+
+ if self.position != self.original_position
+ project.repository.keep_around(self.position.base_sha)
+ project.repository.keep_around(self.position.start_sha)
+ project.repository.keep_around(self.position.head_sha)
+ end
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
new file mode 100644
index 00000000000..ac3a571a1f3
--- /dev/null
+++ b/app/models/environment.rb
@@ -0,0 +1,16 @@
+class Environment < ActiveRecord::Base
+ belongs_to :project, required: true, validate: true
+
+ has_many :deployments
+
+ validates :name,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ def last_deployment
+ deployments.last
+ end
+end
diff --git a/app/models/event.rb b/app/models/event.rb
index 17ee48b91a8..fd736d12359 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -67,7 +67,7 @@ class Event < ActiveRecord::Base
elsif issue? || issue_note?
Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
else
- ((merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target.present?) || milestone?
end
end
@@ -80,7 +80,7 @@ class Event < ActiveRecord::Base
end
def target_title
- target.title if target && target.respond_to?(:title)
+ target.try(:title)
end
def created?
@@ -136,7 +136,7 @@ class Event < ActiveRecord::Base
end
def note?
- target_type == "Note"
+ target.is_a?(Note)
end
def issue?
@@ -266,28 +266,20 @@ class Event < ActiveRecord::Base
branch? && project.default_branch != branch_name
end
- def note_commit_id
- target.commit_id
- end
-
def target_iid
target.respond_to?(:iid) ? target.iid : target_id
end
- def note_short_commit_id
- Commit.truncate_sha(note_commit_id)
- end
-
- def note_commit?
- target.noteable_type == "Commit"
+ def commit_note?
+ target.for_commit?
end
def issue_note?
- note? && target && target.noteable_type == "Issue"
+ note? && target && target.for_issue?
end
- def note_project_snippet?
- target.noteable_type == "Snippet"
+ def project_snippet_note?
+ target.for_snippet?
end
def note_target
@@ -295,19 +287,22 @@ class Event < ActiveRecord::Base
end
def note_target_id
- if note_commit?
+ if commit_note?
target.commit_id
else
target.noteable_id.to_s
end
end
- def note_target_iid
- if note_target.respond_to?(:iid)
- note_target.iid
+ def note_target_reference
+ return unless note_target
+
+ # Commit#to_reference returns the full SHA, but we want the short one here
+ if commit_note?
+ note_target.short_id
else
- note_target_id
- end.to_s
+ note_target.to_reference
+ end
end
def note_target_type
@@ -320,7 +315,7 @@ class Event < ActiveRecord::Base
def body?
if push?
- push_with_commits?
+ push_with_commits? || rm_ref?
elsif note?
true
else
diff --git a/app/models/group.rb b/app/models/group.rb
index aec92e335e6..37631b99701 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -3,11 +3,19 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
+ include AccessRequestable
include Referable
- has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
+ has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :owners,
+ -> { where(members: { access_level: Gitlab::Access::OWNER }) },
+ through: :group_members,
+ source: :user
+
+ has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember'
+
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
@@ -58,6 +66,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}"
end
+ def web_url
+ Gitlab::Routing.url_helpers.group_url(self)
+ end
+
def human_name
name
end
@@ -78,15 +90,11 @@ class Group < Namespace
end
def avatar_url(size = nil)
- if avatar.present?
+ if self[:avatar].present?
[gitlab_config.url, avatar.url].join
end
end
- def owners
- @owners ||= group_members.owners.includes(:user).map(&:user)
- end
-
def add_users(user_ids, access_level, current_user = nil)
user_ids.each do |user_id|
Member.add_user(self.group_members, user_id, access_level, current_user)
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index fde05f729dc..8b87b6c3d64 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -38,7 +38,7 @@ class WebHook < ActiveRecord::Base
basic_auth: auth)
end
- [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ [response.code, 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/issue.rb b/app/models/issue.rb
index 2d4a9b9f19a..60abd47409e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -19,6 +19,8 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ has_many :events, as: :target, dependent: :destroy
+
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
@@ -51,10 +53,18 @@ class Issue < ActiveRecord::Base
end
def self.visible_to_user(user)
- return where(confidential: false) if user.blank?
+ return where('issues.confidential IS NULL OR issues.confidential IS 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))
+ where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def self.reference_prefix
@@ -75,10 +85,14 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
- def self.sort(method)
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
+ def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
- when 'due_date_desc' then order_due_date_desc
+ when 'due_date_desc' then order_due_date_desc
else
super
end
@@ -95,14 +109,13 @@ class Issue < ActiveRecord::Base
end
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
+ ext = all_references(current_user)
+
+ notes_with_associations.each do |object|
+ object.all_references(current_user, extractor: ext)
end
+
+ ext.merge_requests.sort_by(&:iid)
end
# All branches containing the current issue's ID, except for
@@ -139,9 +152,13 @@ class Issue < ActiveRecord::Base
def closed_by_merge_requests(current_user = nil)
return [] unless open?
- notes.system.flat_map do |note|
- note.all_references(current_user).merge_requests
- end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
+ ext = all_references(current_user)
+
+ notes.system.each do |note|
+ note.all_references(current_user, extractor: ext)
+ end
+
+ ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
end
def moved?
diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb
deleted file mode 100644
index 5b21aac5e43..00000000000
--- a/app/models/jira_issue.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class JiraIssue < ExternalIssue
-end
diff --git a/app/models/key.rb b/app/models/key.rb
index d52afda67d1..b9bc38a0436 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -9,7 +9,7 @@ class Key < ActiveRecord::Base
before_validation :strip_white_space, :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
- validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true
+ validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
@@ -26,7 +26,7 @@ class Key < ActiveRecord::Base
end
def publishable_key
- #Removes anything beyond the keytype and key itself
+ # Removes anything beyond the keytype and key itself
self.key.split[0..1].join(' ')
end
diff --git a/app/models/label.rb b/app/models/label.rb
index e5ad11983be..dc5586f5756 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -20,16 +20,26 @@ class Label < ActiveRecord::Base
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
- # Don't allow '?', '&', and ',' for label titles
+ # Don't allow ',' for label titles
validates :title,
presence: true,
- format: { with: /\A[^&\?,]+\z/ },
+ format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
+ before_save :nullify_priority
+
default_scope { order(title: :asc) }
scope :templates, -> { where(template: true) }
+ def self.prioritized
+ where.not(priority: nil).reorder(:priority, :title)
+ end
+
+ def self.unprioritized
+ where(priority: nil)
+ end
+
alias_attribute :name, :title
def self.reference_prefix
@@ -48,8 +58,8 @@ class Label < ActiveRecord::Base
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
- [A-Za-z0-9_-]+ | # String-based single-word label title, or
- "[^&\?,]+" # String-based multi-word label surrounded in quotes
+ [A-Za-z0-9_\-\?&]+ | # String-based single-word label title, or
+ "[^,]+" # String-based multi-word label surrounded in quotes
)
)
}x
@@ -104,7 +114,7 @@ class Label < ActiveRecord::Base
end
def title=(value)
- write_attribute(:title, Sanitize.clean(value.to_s)) if value.present?
+ write_attribute(:title, sanitize_title(value)) if value.present?
end
private
@@ -118,4 +128,12 @@ class Label < ActiveRecord::Base
id
end
end
+
+ def nullify_priority
+ self.priority = nil if priority.blank?
+ end
+
+ def sanitize_title(value)
+ CGI.unescapeHTML(Sanitize.clean(value.to_s))
+ end
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
new file mode 100644
index 00000000000..790dfd4d480
--- /dev/null
+++ b/app/models/legacy_diff_note.rb
@@ -0,0 +1,114 @@
+class LegacyDiffNote < Note
+ include NoteOnDiff
+
+ serialize :st_diff
+
+ validates :line_code, presence: true, line_code: true
+
+ before_create :set_diff
+
+ class << self
+ def build_discussion_id(noteable_type, noteable_id, line_code, active = true)
+ [super(noteable_type, noteable_id), line_code, active].join("-")
+ end
+ end
+
+ def legacy_diff_note?
+ true
+ end
+
+ def diff_attributes
+ { line_code: line_code }
+ end
+
+ def discussion_id
+ @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
+ end
+
+ def diff_file_hash
+ line_code.split('_')[0] if line_code
+ end
+
+ def diff
+ @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
+ end
+
+ def diff_file
+ @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff
+ end
+
+ def diff_line
+ @diff_line ||= diff_file.line_for_line_code(self.line_code)
+ end
+
+ def for_line?(line)
+ !line.meta? && diff_file.line_code(line) == self.line_code
+ end
+
+ # 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 @active if defined?(@active)
+ return true if for_commit?
+ return true unless self.diff
+ return false unless noteable
+
+ noteable_diff = find_noteable_diff
+
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
+
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line.text }
+ else
+ @active = false
+ end
+
+ @active
+ end
+
+ private
+
+ def find_diff
+ return nil unless noteable
+ return @diff if defined?(@diff)
+
+ @diff = noteable.diffs(Commit.max_diff_options).find do |d|
+ d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash
+ end
+ end
+
+ def set_diff
+ # First lets find notes with same diff
+ # before iterating over all mr diffs
+ diff = diff_for_line_code unless for_merge_request?
+ diff ||= find_diff
+
+ self.st_diff = diff.to_hash if diff
+ end
+
+ def diff_for_line_code
+ attributes = {
+ noteable_type: noteable_type,
+ line_code: line_code
+ }
+
+ if for_commit?
+ attributes[:commit_id] = commit_id
+ else
+ attributes[:noteable_id] = noteable_id
+ end
+
+ self.class.where(attributes).last.try(:diff)
+ 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
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index d3060f07fc0..44db3d977fa 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,5 +1,6 @@
class Member < ActiveRecord::Base
include Sortable
+ include Importable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -26,20 +27,26 @@ class Member < ActiveRecord::Base
allow_nil: true
}
- scope :invite, -> { where(user_id: nil) }
- scope :non_invite, -> { where("user_id IS NOT NULL") }
+ scope :invite, -> { where.not(invite_token: nil) }
+ scope :non_invite, -> { where(invite_token: nil) }
+ scope :request, -> { where.not(requested_at: nil) }
+ scope :has_access, -> { where('access_level > 0') }
+
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
+ scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
- after_create :send_invite, if: :invite?
- after_create :create_notification_setting, unless: :invite?
- after_create :post_create_hook, unless: :invite?
- after_update :post_update_hook, unless: :invite?
- after_destroy :post_destroy_hook, unless: :invite?
+
+ after_create :send_invite, if: :invite?, unless: :importing?
+ after_create :send_request, if: :request?, unless: :importing?
+ after_create :create_notification_setting, unless: [:pending?, :importing?]
+ after_create :post_create_hook, unless: [:pending?, :importing?]
+ after_update :post_update_hook, unless: [:pending?, :importing?]
+ after_destroy :post_destroy_hook, unless: :pending?
delegate :name, :username, :email, to: :user, prefix: true
@@ -96,10 +103,31 @@ class Member < ActiveRecord::Base
end
end
+ def real_source_type
+ source_type
+ end
+
def invite?
self.invite_token.present?
end
+ def request?
+ requested_at.present?
+ end
+
+ def pending?
+ invite? || request?
+ end
+
+ def accept_request
+ return false unless request?
+
+ updated = self.update(requested_at: nil)
+ after_accept_request if updated
+
+ updated
+ end
+
def accept_invite!(new_user)
return false unless invite?
@@ -157,6 +185,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
+ def send_request
+ notification_service.new_access_request(self)
+ end
+
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
@@ -177,6 +209,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
+ def after_accept_request
+ post_create_hook
+ end
+
def system_hook_service
SystemHooksService.new
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index f63a0debf1a..2f13d339c89 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
- scope :with_group, ->(group) { where(source_id: group.id) }
- scope :with_user, ->(user) { where(user_id: user.id) }
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -23,6 +20,11 @@ class GroupMember < Member
access_level
end
+ # Because source_type is `Namespace`...
+ def real_source_type
+ 'Group'
+ end
+
private
def send_invite
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 8dae3bb2ef2..f39afc61ce9 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -5,18 +5,16 @@ class ProjectMember < Member
belongs_to :project, class_name: 'Project', foreign_key: 'source_id'
-
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
- scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
- scope :with_user, ->(user) { where(user_id: user.id) }
- class << self
+ before_destroy :delete_member_todos
+ class << self
# Add users to project teams with passed access option
#
# access can be an integer representing a access code
@@ -83,7 +81,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options
end
- def access_roles
+ def access_level_roles
Gitlab::Access.options
end
end
@@ -102,6 +100,10 @@ class ProjectMember < Member
private
+ def delete_member_todos
+ user.todos.where(project_id: source_id).destroy_all if user
+ end
+
def send_invite
notification_service.invite_project_member(self, @raw_invite_token)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5460935366c..393d8a72657 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base
include Referable
include Sortable
include Taskable
+ include Importable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
@@ -11,9 +12,11 @@ class MergeRequest < ActiveRecord::Base
has_one :merge_request_diff, dependent: :destroy
+ has_many :events, as: :target, dependent: :destroy
+
serialize :merge_params, Hash
- after_create :create_merge_request_diff
+ after_create :create_merge_request_diff, unless: :importing?
after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
@@ -82,21 +85,16 @@ class MergeRequest < ActiveRecord::Base
state :cannot_be_merged
around_transition do |merge_request, transition, block|
- merge_request.record_timestamps = false
- begin
- block.call
- ensure
- merge_request.record_timestamps = true
- end
+ Gitlab::Timeless.timeless(merge_request, &block)
end
end
- validates :source_project, presence: true, unless: :allow_broken
+ validates :source_project, presence: true, unless: [:allow_broken, :importing?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
- validate :validate_branches, unless: :allow_broken
+ validate :validate_branches, unless: [:allow_broken, :importing?]
validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
@@ -110,6 +108,8 @@ class MergeRequest < ActiveRecord::Base
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
+ after_save :keep_around_commit
+
def self.reference_prefix
'!'
end
@@ -128,6 +128,10 @@ class MergeRequest < ActiveRecord::Base
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
@@ -156,10 +160,6 @@ class MergeRequest < ActiveRecord::Base
reference
end
- def last_commit
- merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
- end
-
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
@@ -169,15 +169,86 @@ class MergeRequest < ActiveRecord::Base
end
def diff_base_commit
- if merge_request_diff
+ if persisted?
merge_request_diff.base_commit
- elsif source_sha
- self.target_project.merge_base_commit(self.source_sha, self.target_branch)
+ elsif diff_start_commit && diff_head_commit
+ self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
end
end
- def last_commit_short_sha
- last_commit.short_id
+ # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
+ # but we need to get a commit for the "View file @ ..." link by deleted files,
+ # so we find the likely one if we can't get the actual one.
+ # This will not be the actual base commit if the target branch was merged into
+ # the source branch after the merge request was created, but it is good enough
+ # for the specific purpose of linking to a commit.
+ # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
+ # true base commit, so we can't simply have `#diff_base_commit` fall back on
+ # this method.
+ def likely_diff_base_commit
+ first_commit.parent || first_commit
+ end
+
+ def diff_start_commit
+ if persisted?
+ merge_request_diff.start_commit
+ else
+ target_branch_head
+ end
+ end
+
+ def diff_head_commit
+ if persisted?
+ merge_request_diff.head_commit
+ else
+ source_branch_head
+ end
+ end
+
+ def diff_start_sha
+ diff_start_commit.try(:sha)
+ end
+
+ def diff_base_sha
+ diff_base_commit.try(:sha)
+ end
+
+ def diff_head_sha
+ diff_head_commit.try(:sha)
+ end
+
+ # When importing a pull request from GitHub, the old and new branches may no
+ # longer actually exist by those names, but we need to recreate the merge
+ # request diff with the right source and target shas.
+ # We use these attributes to force these to the intended values.
+ attr_writer :target_branch_sha, :source_branch_sha
+
+ def source_branch_head
+ source_branch_ref = @source_branch_sha || source_branch
+ source_project.repository.commit(source_branch) if source_branch_ref
+ end
+
+ def target_branch_head
+ target_branch_ref = @target_branch_sha || target_branch
+ target_project.repository.commit(target_branch) if target_branch_ref
+ end
+
+ def target_branch_sha
+ target_branch_head.try(:sha)
+ end
+
+ def source_branch_sha
+ source_branch_head.try(:sha)
+ end
+
+ def diff_refs
+ return unless diff_start_commit || diff_base_commit
+
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: diff_base_sha,
+ start_sha: diff_start_sha,
+ head_sha: diff_head_sha
+ )
end
def validate_branches
@@ -214,21 +285,30 @@ class MergeRequest < ActiveRecord::Base
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
- reload_code
+ reload_diff
end
end
- def reload_code
- if merge_request_diff && open?
- merge_request_diff.reload_content
- end
+ def reload_diff
+ return unless merge_request_diff && open?
+
+ old_diff_refs = self.diff_refs
+
+ merge_request_diff.reload_content
+
+ new_diff_refs = self.diff_refs
+
+ update_diff_notes_positions(
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs
+ )
end
def check_if_can_be_merged
return unless unchecked?
can_be_merged =
- !broken? && project.repository.can_be_merged?(source_sha, target_branch)
+ !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
if can_be_merged
mark_as_mergeable
@@ -238,11 +318,11 @@ class MergeRequest < ActiveRecord::Base
end
def merge_event
- self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
+ @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
end
def closed_event
- self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
+ @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
@@ -255,20 +335,21 @@ class MergeRequest < ActiveRecord::Base
self.title.sub(WIP_REGEX, "")
end
- def mergeable?
- return false unless open? && !work_in_progress? && !broken?
+ def mergeable?(skip_ci_check: false)
+ return false unless mergeable_state?(skip_ci_check: skip_ci_check)
check_if_can_be_merged
can_be_merged?
end
- def gitlab_merge_status
- if work_in_progress?
- "work_in_progress"
- else
- merge_status_name
- end
+ def mergeable_state?(skip_ci_check: false)
+ return false unless open?
+ return false if work_in_progress?
+ return false if broken?
+ return false unless skip_ci_check || mergeable_ci_state?
+
+ true
end
def can_cancel_merge_when_build_succeeds?(current_user)
@@ -279,7 +360,19 @@ class MergeRequest < ActiveRecord::Base
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project) &&
- last_commit == source_project.commit(source_branch)
+ diff_head_commit == source_branch_head
+ end
+
+ def should_remove_source_branch?
+ merge_params['should_remove_source_branch'].present?
+ end
+
+ def force_remove_source_branch?
+ merge_params['force_remove_source_branch'].present?
+ end
+
+ def remove_source_branch?
+ should_remove_source_branch? || force_remove_source_branch?
end
def mr_and_commit_notes
@@ -297,20 +390,6 @@ class MergeRequest < ActiveRecord::Base
)
end
- # Returns the raw diff for this merge request
- #
- # see "git diff"
- def to_diff
- target_project.repository.diff_text(diff_base_commit.sha, source_sha)
- end
-
- # Returns the commit as a series of email patches.
- #
- # see "git format-patch"
- def to_patch
- target_project.repository.format_patch(diff_base_commit.sha, source_sha)
- end
-
def hook_attrs
attrs = {
source: source_project.try(:hook_attrs),
@@ -319,8 +398,8 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress?
}
- if last_commit
- attrs.merge!(last_commit: last_commit.hook_attrs)
+ if diff_head_commit
+ attrs.merge!(last_commit: diff_head_commit.hook_attrs)
end
attributes.merge!(attrs)
@@ -422,7 +501,10 @@ class MergeRequest < ActiveRecord::Base
self.merge_when_build_succeeds = false
self.merge_user = nil
- self.merge_params = nil
+ if merge_params
+ merge_params.delete('should_remove_source_branch')
+ merge_params.delete('commit_message')
+ end
self.save
end
@@ -466,7 +548,13 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_merged_by?(user)
- ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
+ ::Gitlab::GitAccess.new(user, project, 'web').can_push_to_branch?(target_branch)
+ end
+
+ def mergeable_ci_state?
+ return true unless project.only_allow_merge_if_build_succeeds?
+
+ !pipeline || pipeline.success?
end
def state_human_name
@@ -489,18 +577,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- def target_sha
- @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
- end
-
- def source_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
target_project.repository.fetch_ref(
source_project.repository.path_to_repo,
@@ -513,12 +589,12 @@ class MergeRequest < ActiveRecord::Base
"refs/merge-requests/#{iid}/head"
end
- def ref_is_fetched?
- File.exists?(File.join(project.repository.path_to_repo, ref_path))
+ def ref_fetched?
+ project.repository.ref_exists?(ref_path)
end
def ensure_ref_fetched
- fetch_ref unless ref_is_fetched?
+ fetch_ref unless ref_fetched?
end
def in_locked_state
@@ -533,10 +609,10 @@ class MergeRequest < ActiveRecord::Base
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
+ if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
cache = {
- source_sha: source_sha,
- target_sha: target_sha,
+ source_sha: source_branch_sha,
+ target_sha: target_branch_sha,
diverged_commits_count: compute_diverged_commits_count
}
Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
@@ -546,9 +622,9 @@ class MergeRequest < ActiveRecord::Base
end
def compute_diverged_commits_count
- return 0 unless source_sha && target_sha
+ return 0 unless source_branch_sha && target_branch_sha
- Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
+ Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
end
private :compute_diverged_commits_count
@@ -556,14 +632,8 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def ci_commit
- @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
- end
-
- def diff_refs
- return nil unless diff_base_commit
-
- [diff_base_commit, last_commit]
+ def pipeline
+ @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
end
def merge_commit
@@ -577,4 +647,38 @@ class MergeRequest < ActiveRecord::Base
def can_be_cherry_picked?
merge_commit
end
+
+ def support_new_diff_notes?
+ diff_refs && diff_refs.complete?
+ end
+
+ def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
+ return unless support_new_diff_notes?
+ return if new_diff_refs == old_diff_refs
+
+ active_diff_notes = self.notes.diff_notes.select do |note|
+ note.new_diff_note? && note.active?(old_diff_refs)
+ end
+
+ return if active_diff_notes.empty?
+
+ paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq
+
+ service = Notes::DiffPositionUpdateService.new(
+ self.project,
+ nil,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ paths: paths
+ )
+
+ active_diff_notes.each do |note|
+ service.execute(note)
+ Gitlab::Timeless.timeless(note, &:save)
+ end
+ end
+
+ def keep_around_commit
+ project.repository.keep_around(self.merge_commit_sha)
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index eb42c07b9b9..ba235750aeb 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,12 +1,13 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
+ include Importable
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
- delegate :target_branch, :source_branch, to: :merge_request, prefix: nil
+ delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
state_machine :state, initial: :empty do
state :collected
@@ -22,7 +23,8 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content
+ after_create :reload_content, unless: :importing?
+ after_save :keep_around_commits, unless: :importing?
def reload_content
reload_commits
@@ -37,9 +39,9 @@ class MergeRequestDiff < ActiveRecord::Base
if options[:ignore_whitespace_change]
@diffs_no_whitespace ||= begin
compare = Gitlab::Git::Compare.new(
- self.repository.raw_repository,
- self.target_branch,
- self.source_sha,
+ repository.raw_repository,
+ self.start_commit_sha || self.target_branch_sha,
+ self.head_commit_sha || self.source_branch_sha,
)
compare.diffs(options)
end
@@ -61,120 +63,173 @@ class MergeRequestDiff < ActiveRecord::Base
end
def base_commit
- return nil unless self.base_commit_sha
+ return unless self.base_commit_sha
- merge_request.target_project.commit(self.base_commit_sha)
+ project.commit(self.base_commit_sha)
end
- def last_commit_short_sha
- @last_commit_short_sha ||= last_commit.short_id
- end
+ def start_commit
+ return unless self.start_commit_sha
- def dump_commits(commits)
- commits.map(&:to_hash)
+ project.commit(self.start_commit_sha)
end
- def load_commits(array)
- array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
- end
+ def head_commit
+ return last_commit unless self.head_commit_sha
- def dump_diffs(diffs)
- if diffs.respond_to?(:map)
- diffs.map(&:to_hash)
- end
+ project.commit(self.head_commit_sha)
end
- def load_diffs(raw, options)
- if raw.respond_to?(:each)
- Gitlab::Git::DiffCollection.new(raw, options)
- else
- Gitlab::Git::DiffCollection.new([])
- end
+ def compare
+ @compare ||=
+ begin
+ # Update ref for merge request
+ merge_request.fetch_ref
+
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ self.target_branch_sha,
+ self.source_branch_sha
+ )
+ end
end
+ private
+
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
commits = compare.commits
if commits.present?
- commits = Commit.decorate(commits, merge_request.source_project).
- sort_by(&:created_at).
- reverse
+ commits = Commit.decorate(commits, merge_request.source_project).reverse
end
commits
end
+ def dump_commits(commits)
+ commits.map(&:to_hash)
+ end
+
+ def load_commits(array)
+ array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
+ end
+
# Reload all commits related to current merge request from repo
# and save it as array of hashes in st_commits db field
def reload_commits
+ new_attributes = {}
+
commit_objects = unmerged_commits
if commit_objects.present?
- self.st_commits = dump_commits(commit_objects)
+ new_attributes[:st_commits] = dump_commits(commit_objects)
end
- save
+ update_columns_serialized(new_attributes)
+ end
+
+ # Collect array of Git::Diff objects
+ # between target and source branches
+ def unmerged_diffs
+ compare.diffs(Commit.max_diff_options)
+ end
+
+ def dump_diffs(diffs)
+ if diffs.respond_to?(:map)
+ diffs.map(&:to_hash)
+ end
+ end
+
+ def load_diffs(raw, options)
+ if raw.respond_to?(:each)
+ Gitlab::Git::DiffCollection.new(raw, options)
+ else
+ Gitlab::Git::DiffCollection.new([])
+ end
end
# Reload diffs between branches related to current merge request from repo
# and save it as array of hashes in st_diffs db field
def reload_diffs
+ new_attributes = {}
new_diffs = []
if commits.size.zero?
- self.state = :empty
+ new_attributes[:state] = :empty
else
diff_collection = unmerged_diffs
if diff_collection.overflow?
# Set our state to 'overflow' to make the #empty? and #collected?
# methods (generated by StateMachine) return false.
- self.state = :overflow
+ new_attributes[:state] = :overflow
end
- self.real_size = diff_collection.real_size
+ new_attributes[:real_size] = diff_collection.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
- self.state = :collected
+ new_attributes[:state] = :collected
end
end
- self.st_diffs = new_diffs
+ new_attributes[:st_diffs] = new_diffs
- self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch)
+ new_attributes[:start_commit_sha] = self.target_branch_sha
+ new_attributes[:head_commit_sha] = self.source_branch_sha
+ new_attributes[:base_commit_sha] = branch_base_sha
- self.save
+ update_columns_serialized(new_attributes)
+
+ keep_around_commits
end
- # Collect array of Git::Diff objects
- # between target and source branches
- def unmerged_diffs
- compare.diffs(Commit.max_diff_options)
+ def project
+ merge_request.target_project
end
def repository
- merge_request.target_project.repository
+ project.repository
end
- def source_sha
- source_commit = merge_request.source_project.commit(source_branch)
- source_commit.try(:sha)
+ def branch_base_commit
+ return unless self.source_branch_sha && self.target_branch_sha
+
+ project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
end
- def compare
- @compare ||=
- begin
- # Update ref for merge request
- merge_request.fetch_ref
+ def branch_base_sha
+ branch_base_commit.try(:sha)
+ end
- Gitlab::Git::Compare.new(
- self.repository.raw_repository,
- self.target_branch,
- self.source_sha
- )
- end
+ #
+ # #save or #update_attributes providing changes on serialized attributes do a lot of
+ # serialization and deserialization calls resulting in bad performance.
+ # Using #update_columns solves the problem with just one YAML.dump per serialized attribute that we provide.
+ # As a tradeoff we need to reload the current instance to properly manage time objects on those serialized
+ # attributes. So to keep the same behaviour as the attribute assignment we reload the instance.
+ # The difference is in the usage of
+ # #write_attribute= (#update_attributes) and #raw_write_attribute= (#update_columns)
+ #
+ # Ex:
+ #
+ # new_attributes[:st_commits].first.slice(:committed_date)
+ # => {:committed_date=>2014-02-27 11:01:38 +0200}
+ # YAML.load(YAML.dump(new_attributes[:st_commits].first.slice(:committed_date)))
+ # => {:committed_date=>2014-02-27 10:01:38 +0100}
+ #
+ def update_columns_serialized(new_attributes)
+ return unless new_attributes.any?
+
+ update_columns(new_attributes.merge(updated_at: current_time_from_proper_timezone))
+ reload
+ end
+
+ def keep_around_commits
+ repository.keep_around(target_branch_sha)
+ repository.keep_around(source_branch_sha)
+ repository.keep_around(branch_base_sha)
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e4fdd23badb..2bd7f198030 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -17,6 +17,7 @@ class Milestone < ActiveRecord::Base
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
+ has_many :events, as: :target, dependent: :destroy
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
@@ -59,25 +60,67 @@ class Milestone < ActiveRecord::Base
end
end
+ def self.reference_prefix
+ '%'
+ end
+
def self.reference_pattern
- nil
+ # NOTE: The iid pattern only matches when all characters on the expression
+ # are digits, so it will match %2 but not %2.1 because that's probably a
+ # milestone name and we want it to be matched as such.
+ @reference_pattern ||= %r{
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
+ (?:
+ (?<milestone_iid>
+ \d+(?!\S\w)\b # Integer-based milestone iid, or
+ ) |
+ (?<milestone_name>
+ [^"\s]+\b | # String-based single-word milestone title, or
+ "[^"]+" # String-based multi-word milestone surrounded in quotes
+ )
+ )
+ }x
end
def self.link_reference_pattern
@link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
- def self.upcoming
- self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
- end
+ def self.upcoming_ids_by_projects(projects)
+ rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now)
- def to_reference(from_project = nil)
- escaped_title = self.title.gsub("]", "\\]")
+ if Gitlab::Database.postgresql?
+ rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
+ else
+ rel.
+ group(:project_id).
+ having('due_date = MIN(due_date)').
+ pluck(:id, :project_id, :due_date).
+ map(&:first)
+ end
+ end
- h = Gitlab::Routing.url_helpers
- url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
+ ##
+ # Returns the String necessary to reference this Milestone in Markdown
+ #
+ # format - Symbol format to use (default: :iid, optional: :name)
+ #
+ # Examples:
+ #
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(format: :name) # => "%\"goal\""
+ # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1"
+ #
+ def to_reference(from_project = nil, format: :iid)
+ format_reference = milestone_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
- "[#{escaped_title}](#{url})"
+ if cross_project_reference?(from_project)
+ project.to_reference + reference
+ else
+ reference
+ end
end
def reference_link_text(from_project = nil)
@@ -149,4 +192,16 @@ class Milestone < ActiveRecord::Base
issues.where(id: ids).
update_all(["position = CASE #{conditions} ELSE position END", *pairs])
end
+
+ private
+
+ def milestone_format_reference(format = :iid)
+ raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ iid
+ end
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9c942a8f4e3..8b52cc824cd 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -21,8 +21,10 @@ class Namespace < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
- after_create :ensure_dir_exist
after_update :move_dir, if: :path_changed?
+
+ # Save the storage paths before the projects are destroyed to use them on after destroy
+ before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
after_destroy :rm_dir
scope :root, -> { where('type IS NULL') }
@@ -87,48 +89,40 @@ class Namespace < ActiveRecord::Base
owner_name
end
- def ensure_dir_exist
- gitlab_shell.add_namespace(path)
- end
+ def move_dir
+ if any_project_has_container_registry_tags?
+ raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ end
- def rm_dir
- # Move namespace directory into trash.
- # We will remove it later async
- new_path = "#{path}+#{id}+deleted"
+ # Move the namespace directory in all storages paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, path_was)
- if gitlab_shell.mv_namespace(path, new_path)
- message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
- Gitlab::AppLogger.info message
+ unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
- # Remove namespace directroy async with delay so
- # GitLab has time to remove all projects first
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path)
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+
+ # If repositories moved successfully we need to
+ # send update instructions to users.
+ # However we cannot allow rollback since we moved namespace dir
+ # So we basically we mute exceptions in next actions
+ begin
+ send_update_instructions
+ rescue
+ # Returning false does not rollback after_* transaction but gives
+ # us information about failing some of tasks
+ false
end
end
- def move_dir
- # Ensure old directory exists before moving it
- gitlab_shell.add_namespace(path_was)
-
- if gitlab_shell.mv_namespace(path_was, path)
- Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
-
- # If repositories moved successfully we need to
- # send update instructions to users.
- # However we cannot allow rollback since we moved namespace dir
- # So we basically we mute exceptions in next actions
- begin
- send_update_instructions
- rescue
- # Returning false does not rollback after_* transaction but gives
- # us information about failing some of tasks
- false
- end
- else
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Exception.new('namespace directory cannot be moved')
- end
+ def any_project_has_container_registry_tags?
+ projects.any?(&:has_container_registry_tags?)
end
def send_update_instructions
@@ -144,4 +138,33 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project)
projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
end
+
+ private
+
+ def repository_storage_paths
+ # We need to get the storage paths for all the projects, even the ones that are
+ # pending delete. Unscoping also get rids of the default order, which causes
+ # problems with SELECT DISTINCT.
+ Project.unscoped do
+ projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
+ end
+ end
+
+ def rm_dir
+ # Remove the namespace directory in all storages paths used by member projects
+ @old_repository_storage_paths.each do |repository_storage_path|
+ # Move namespace directory into trash.
+ # We will remove it later async
+ new_path = "#{path}+#{id}+deleted"
+
+ if gitlab_shell.mv_namespace(repository_storage_path, path, new_path)
+ message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
+ Gitlab::AppLogger.info message
+
+ # Remove namespace directroy async with delay so
+ # GitLab has time to remove all projects first
+ GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
+ end
+ end
+ end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index f4e90125373..345041a6ad1 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -22,9 +22,16 @@ module Network
def collect_notes
h = Hash.new(0)
- @project.notes.where('noteable_type = ?' ,"Commit").group('notes.commit_id').select('notes.commit_id, count(notes.id) as note_count').each do |item|
- h[item.commit_id] = item.note_count.to_i
- end
+
+ @project
+ .notes
+ .where('noteable_type = ?', 'Commit')
+ .group('notes.commit_id')
+ .select('notes.commit_id, count(notes.id) as note_count')
+ .each do |item|
+ h[item.commit_id] = item.note_count.to_i
+ end
+
h
end
@@ -47,7 +54,7 @@ module Network
@map = {}
@reserved = {}
- @commits.each_with_index do |c,i|
+ @commits.each_with_index do |c, i|
c.time = i
days[i] = c.committed_date
@map[c.id] = c
@@ -89,7 +96,7 @@ module Network
end
end
- if self.class.max_count / 2 < offset then
+ if self.class.max_count / 2 < offset
# get max index that commit is displayed in the center.
offset - self.class.max_count / 2
else
@@ -109,7 +116,7 @@ module Network
end
def commits_sort_by_ref
- @commits.sort do |a,b|
+ @commits.sort do |a, b|
if include_ref?(a)
-1
elsif include_ref?(b)
@@ -130,7 +137,7 @@ module Network
commit.parents(@map).each do |parent|
range = commit.time..parent.time
- space = if commit.space >= parent.space then
+ space = if commit.space >= parent.space
find_free_parent_space(range, parent.space, -1, commit.space)
else
find_free_parent_space(range, commit.space, -1, parent.space)
@@ -144,7 +151,7 @@ module Network
end
def find_free_parent_space(range, space_base, space_step, space_default)
- if is_overlap?(range, space_default) then
+ if is_overlap?(range, space_default)
find_free_space(range, space_step, space_base, space_default)
else
space_default
@@ -155,9 +162,9 @@ module Network
range.each do |i|
if i != range.first &&
i != range.last &&
- @commits[i].spaces.include?(overlap_space) then
+ @commits[i].spaces.include?(overlap_space)
- return true;
+ return true
end
end
@@ -198,7 +205,7 @@ module Network
# Visit branching chains
leaves.each do |l|
parents = l.parents(@map).select{|p| p.space.zero?}
- for p in parents
+ parents.each do |p|
place_chain(p, l.time)
end
end
@@ -216,7 +223,7 @@ module Network
end
def mark_reserved(time_range, space)
- for day in time_range
+ time_range.each do |day|
@reserved[day].push(space)
end
end
@@ -225,15 +232,15 @@ module Network
space_default ||= space_base
reserved = []
- for day in time_range
+ time_range.each do |day|
reserved.push(*@reserved[day])
end
reserved.uniq!
space = space_default
- while reserved.include?(space) do
+ while reserved.include?(space)
space += space_step
- if space < space_base then
+ if space < space_base
space_step *= -1
space = space_base + space_step
end
@@ -253,7 +260,7 @@ module Network
leaves = []
leaves.push(commit) if commit.space.zero?
- while true
+ loop do
return leaves if commit.parents(@map).count.zero?
commit = commit.parents(@map).first
diff --git a/app/models/note.rb b/app/models/note.rb
index f26aa1bf63f..ffffd0c0838 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -1,13 +1,18 @@
-require 'carrierwave/orm/activerecord'
-
class Note < ActiveRecord::Base
+ extend ActiveModel::Naming
include Gitlab::CurrentSettings
include Participable
include Mentionable
+ include Awardable
+ include Importable
+
+ # Attribute containing rendered and redacted Markdown as generated by
+ # Banzai::ObjectRenderer.
+ attr_accessor :note_html
default_value_for :system, false
- attr_mentionable :note, cache: true, pipeline: :note
+ attr_mentionable :note, pipeline: :note
participant :author
belongs_to :project
@@ -16,99 +21,85 @@ class Note < ActiveRecord::Base
belongs_to :updated_by, class_name: "User"
has_many :todos, dependent: :destroy
+ has_many :events, as: :target, 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!
+ delegate :title, to: :noteable, allow_nil: true
validates :note, :project, presence: true
- validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
- validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
- validates :line_code, line_code: true, allow_blank: true
+
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
- validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
- validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
+ validates :noteable_type, presence: true
+ validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
+ validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
+ validate unless: [:for_commit?, :importing?] do |note|
+ unless note.noteable.try(:project) == note.project
+ errors.add(:invalid_project, 'Note and noteable project mismatch')
+ end
+ end
+
mount_uploader :attachment, AttachmentUploader
# Scopes
- scope :awards, ->{ where(is_award: true) }
- 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 :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
+ scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) }
+
+ scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
+ scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
scope :with_associations, -> do
+ # FYI noteable cannot be loaded for LegacyDiffNote for commits
includes(:author, :noteable, :updated_by,
project: [:project_members, { group: [:group_members] }])
end
- serialize :st_diff
- before_create :set_diff, if: ->(n) { n.line_code.present? }
+ before_validation :clear_blank_line_code!
+ after_save :keep_around_commit
class << self
- def discussions_from_notes(notes)
- discussion_ids = []
- discussions = []
-
- notes.each do |note|
- next if discussion_ids.include?(note.discussion_id)
-
- # don't group notes for the main target
- if !note.for_diff_line? && note.for_merge_request?
- discussions << [note]
- else
- discussions << notes.select do |other_note|
- note.discussion_id == other_note.discussion_id
- end
- discussion_ids << note.discussion_id
- end
- end
+ def model_name
+ ActiveModel::Name.new(self, nil, 'note')
+ end
+
+ def build_discussion_id(noteable_type, noteable_id)
+ [:discussion, noteable_type.try(:underscore), noteable_id].join("-")
+ end
- discussions
+ def discussions
+ all.group_by(&:discussion_id).values
end
- def build_discussion_id(type, id, line_code)
- [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
+ def grouped_diff_notes
+ diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
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.
+ # query - The search query as a String.
+ # as_user - Limit results to those viewable by a specific user
#
# Returns an ActiveRecord::Relation.
- def search(query)
+ def search(query, as_user: nil)
table = arel_table
pattern = "%#{query}%"
- where(table[:note].matches(pattern))
- end
-
- def grouped_awards
- notes = {}
-
- awards.select(:note).distinct.map do |note|
- notes[note.note] = where(note: note.note)
- end
-
- notes["thumbsup"] ||= Note.none
- notes["thumbsdown"] ||= Note.none
-
- notes
+ Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
+ where(table[:note].matches(pattern)).
+ merge(Issue.visible_to_user(as_user))
end
end
@@ -116,167 +107,43 @@ class Note < ActiveRecord::Base
system && SystemNoteService.cross_reference?(note)
end
- def max_attachment_size
- current_application_settings.max_attachment_size.megabytes.to_i
+ def diff_note?
+ false
end
- def find_diff
- return nil unless noteable
- return @diff if defined?(@diff)
-
- # 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
+ def legacy_diff_note?
+ false
end
- def hook_attrs
- attributes
+ def new_diff_note?
+ false
end
- def set_diff
- # First lets find notes with same diff
- # before iterating over all mr diffs
- diff = diff_for_line_code unless for_merge_request?
- diff ||= find_diff
-
- self.st_diff = diff.to_hash if diff
- end
-
- def diff
- @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
- end
-
- def diff_for_line_code
- Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
- end
-
- # 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_diff = find_noteable_diff
-
- if noteable_diff
- parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
-
- @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
- else
- @active = false
- end
-
- @active
- end
-
- def diff_file_index
- line_code.split('_')[0] if line_code
- end
-
- def diff_file_name
- diff.new_path if diff
+ true
end
- def file_path
- if diff.new_path.present?
- diff.new_path
- elsif diff.old_path.present?
- diff.old_path
- end
- end
-
- def diff_old_line
- line_code.split('_')[1].to_i if line_code
- end
-
- def diff_new_line
- line_code.split('_')[2].to_i if line_code
- end
-
- def generate_line_code(line)
- Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
- end
-
- def diff_line
- return @diff_line if @diff_line
-
- if diff
- diff_lines.each do |line|
- if generate_line_code(line) == self.line_code
- @diff_line = line.text
- end
- end
- end
-
- @diff_line
- end
-
- def diff_line_type
- return @diff_line_type if @diff_line_type
-
- if diff
- diff_lines.each do |line|
- if generate_line_code(line) == self.line_code
- @diff_line_type = line.type
- end
- end
- end
-
- @diff_line_type
- end
-
- def truncated_diff_lines
- max_number_of_lines = 16
- prev_match_line = nil
- prev_lines = []
-
- highlighted_diff_lines.each do |line|
- if line.type == "match"
- prev_lines.clear
- prev_match_line = line
+ def discussion_id
+ @discussion_id ||=
+ if for_merge_request?
+ [:discussion, :note, id].join("-")
else
- prev_lines << line
-
- break if generate_line_code(line) == self.line_code
-
- prev_lines.shift if prev_lines.length >= max_number_of_lines
+ self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
end
- end
-
- prev_lines
- end
-
- def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
end
- def highlighted_diff_lines
- Gitlab::Diff::Highlight.new(diff_lines).highlight
+ def max_attachment_size
+ current_application_settings.max_attachment_size.megabytes.to_i
end
- def discussion_id
- @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
+ def hook_attrs
+ attributes
end
def for_commit?
noteable_type == "Commit"
end
- def for_commit_diff_line?
- for_commit? && for_diff_line?
- end
-
- def for_diff_line?
- line_code.present?
- end
-
def for_issue?
noteable_type == "Issue"
end
@@ -285,10 +152,6 @@ class Note < ActiveRecord::Base
noteable_type == "MergeRequest"
end
- def for_merge_request_diff_line?
- for_merge_request? && for_diff_line?
- end
-
def for_snippet?
noteable_type == "Snippet"
end
@@ -325,50 +188,28 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def downvote?
- is_award && note == "thumbsdown"
- end
-
- def upvote?
- is_award && note == "thumbsup"
- end
-
def editable?
- !system? && !is_award
+ !system?
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
- # and changes content of the note to award name.
- #
- # Method is executed as a before_validation callback.
- #
- def set_award!
- return unless awards_supported? && contains_emoji_only?
-
- self.is_award = true
- self.note = award_emoji_name
+ def award_emoji?
+ can_be_award_emoji? && contains_emoji_only?
end
- private
+ def emoji_awardable?
+ !system?
+ end
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?
- (for_issue? || for_merge_request?) && !for_diff_line?
+ def can_be_award_emoji?
+ noteable.is_a?(Awardable)
end
def contains_emoji_only?
@@ -377,6 +218,12 @@ class Note < ActiveRecord::Base
def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
- AwardEmoji.normilize_emoji_name(original_name)
+ Gitlab::AwardEmoji.normalize_emoji_name(original_name)
+ end
+
+ private
+
+ def keep_around_commit
+ project.repository.keep_around(self.commit_id)
end
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 5001738f411..121b598b8f3 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,20 +1,44 @@
class NotificationSetting < ActiveRecord::Base
- enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
+ enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
belongs_to :user
belongs_to :source, polymorphic: true
+ belongs_to :project, foreign_key: 'source_id'
validates :user, presence: true
- validates :source, presence: true
validates :level, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
scope :for_groups, -> { where(source_type: 'Namespace') }
- scope :for_projects, -> { where(source_type: 'Project') }
+
+ # Exclude projects not included by the Project model's default scope (those that are
+ # pending delete).
+ #
+ scope :for_projects, -> do
+ includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil })
+ end
+
+ EMAIL_EVENTS = [
+ :new_note,
+ :new_issue,
+ :reopen_issue,
+ :close_issue,
+ :reassign_issue,
+ :new_merge_request,
+ :reopen_merge_request,
+ :close_merge_request,
+ :reassign_merge_request,
+ :merge_merge_request
+ ]
+
+ store :events, accessors: EMAIL_EVENTS, coder: JSON
+
+ before_create :set_events
+ before_save :events_to_boolean
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
@@ -25,4 +49,21 @@ class NotificationSetting < ActiveRecord::Base
setting
end
+
+ # Set all event attributes to false when level is not custom or being initialized for UX reasons
+ def set_events
+ return if custom?
+
+ EMAIL_EVENTS.each do |event|
+ events[event] = false
+ end
+ end
+
+ # Validates store accessors values as boolean
+ # It is a text field so it does not cast correct boolean values in JSON
+ def events_to_boolean
+ EMAIL_EVENTS.each do |event|
+ events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
+ end
+ end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
new file mode 100644
index 00000000000..c4b095e0c04
--- /dev/null
+++ b/app/models/personal_access_token.rb
@@ -0,0 +1,20 @@
+class PersonalAccessToken < ActiveRecord::Base
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ belongs_to :user
+
+ scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+ scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+
+ def self.generate(params)
+ personal_access_token = self.new(params)
+ personal_access_token.ensure_token
+ personal_access_token
+ end
+
+ def revoke!
+ self.revoked = true
+ self.save
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 418b85e028a..a66b750cd48 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
+ include AccessRequestable
include Referable
include Sortable
include AfterCommitQueue
@@ -22,8 +23,13 @@ class Project < ActiveRecord::Base
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for :container_registry_enabled, gitlab_config_features.container_registry
+ default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
+ after_create :ensure_dir_exist
+ after_save :ensure_dir_exist, if: :namespace_id_changed?
+
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
def set_last_activity_at
@@ -49,6 +55,8 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
+ alias_attribute :title, :name
+
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
@@ -77,7 +85,8 @@ class Project < ActiveRecord::Base
has_one :jira_service, dependent: :destroy
has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy
- has_one :gitlab_issue_tracker_service, dependent: :destroy
+ has_one :bugzilla_service, dependent: :destroy
+ has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
@@ -99,8 +108,13 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+
+ has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+ alias_method :members, :project_members
has_many :users, through: :project_members
+
+ has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
@@ -116,12 +130,14 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
- has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
+ has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :environments, dependent: :destroy
+ has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -143,13 +159,10 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
- validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
- validates :import_url,
- url: { protocols: %w(ssh git http https) },
- if: :external_import?
+ validates :import_url, addressable_url: true, if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
@@ -157,6 +170,10 @@ class Project < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
+ validate :check_wiki_path_conflict
+ validates :repository_storage,
+ presence: true,
+ inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
add_authentication_token_field :runners_token
before_save :ensure_runners_token
@@ -168,17 +185,17 @@ class Project < ActiveRecord::Base
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
- scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
- scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
- scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
- scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
- scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
+ scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
+ scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
+
+ scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
+ scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
state_machine :import_status, initial: :none do
event :import_start do
@@ -201,23 +218,10 @@ class Project < ActiveRecord::Base
state :finished
state :failed
- after_transition any => :started, do: :schedule_add_import_job
- after_transition any => :finished, do: :clear_import_data
+ after_transition any => :finished, do: :reset_cache_and_import_attrs
end
class << self
- def abandoned
- where('projects.last_activity_at < ?', 6.months.ago)
- end
-
- def with_push
- joins(:events).where('events.action = ?', Event::PUSHED)
- end
-
- def active
- 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
@@ -263,24 +267,85 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern))
end
- def find_with_namespace(id)
- namespace_path, project_path = id.split('/', 2)
+ # Finds a single project for the given path.
+ #
+ # path - The full project path (including namespace path).
+ #
+ # Returns a Project, or nil if no project could be found.
+ def find_with_namespace(path)
+ namespace_path, project_path = path.split('/', 2)
+
+ return unless namespace_path && project_path
- return nil if !namespace_path || !project_path
+ namespace_path = connection.quote(namespace_path)
+ project_path = connection.quote(project_path)
- # Use of unscoped ensures we're not secretly adding any ORDER BYs, which
- # have a negative impact on performance (and aren't needed for this
- # query).
- projects = unscoped.
- joins(:namespace).
- iwhere('namespaces.path' => namespace_path)
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
+ "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
- projects.find_by('projects.path' => project_path) ||
- projects.iwhere('projects.path' => project_path).take
+ where_paths_in([path]).reorder(order_sql).take
end
- def find_by_ci_id(id)
- find_by(ci_id: id.to_i)
+ # Builds a relation to find multiple projects by their full paths.
+ #
+ # Each path must be in the following format:
+ #
+ # namespace_path/project_path
+ #
+ # For example:
+ #
+ # gitlab-org/gitlab-ce
+ #
+ # Usage:
+ #
+ # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ #
+ # This would return the projects with the full paths matching the values
+ # given.
+ #
+ # paths - An Array of full paths (namespace path + project path) for which
+ # to find the projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def where_paths_in(paths)
+ wheres = []
+ cast_lower = Gitlab::Database.postgresql?
+
+ paths.each do |path|
+ namespace_path, project_path = path.split('/', 2)
+
+ next unless namespace_path && project_path
+
+ namespace_path = connection.quote(namespace_path)
+ project_path = connection.quote(project_path)
+
+ where = "(namespaces.path = #{namespace_path}
+ AND projects.path = #{project_path})"
+
+ if cast_lower
+ where = "(
+ #{where}
+ OR (
+ LOWER(namespaces.path) = LOWER(#{namespace_path})
+ AND LOWER(projects.path) = LOWER(#{project_path})
+ )
+ )"
+ end
+
+ wheres << where
+ end
+
+ if wheres.empty?
+ none
+ else
+ joins(:namespace).where(wheres.join(' OR '))
+ end
end
def visibility_levels
@@ -314,11 +379,16 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
end
- def visible_to_user(user)
- where(id: user.authorized_projects.select(:id).reorder(nil))
+ # Deletes gitlab project export files older than 24 hours
+ def remove_gitlab_exports!
+ Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
end
end
+ def repository_storage_path
+ Gitlab.config.repositories.storages[repository_storage]
+ end
+
def team
@team ||= ProjectTeam.new(self)
end
@@ -327,8 +397,36 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self)
end
- def commit(id = 'HEAD')
- repository.commit(id)
+ def container_registry_path_with_namespace
+ path_with_namespace.downcase
+ end
+
+ def container_registry_repository
+ return unless Gitlab.config.registry.enabled
+
+ @container_registry_repository ||= begin
+ token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace)
+ url = Gitlab.config.registry.api_url
+ host_port = Gitlab.config.registry.host_port
+ registry = ContainerRegistry::Registry.new(url, token: token, path: host_port)
+ registry.repository(container_registry_path_with_namespace)
+ end
+ end
+
+ def container_registry_repository_url
+ if Gitlab.config.registry.enabled
+ "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}"
+ end
+ end
+
+ def has_container_registry_tags?
+ return unless container_registry_repository
+
+ container_registry_repository.tags.any?
+ end
+
+ def commit(ref = 'HEAD')
+ repository.commit(ref)
end
def merge_base_commit(first_commit_id, second_commit_id)
@@ -340,10 +438,6 @@ class Project < ActiveRecord::Base
id && persisted?
end
- def schedule_add_import_job
- run_after_commit(:add_import_job)
- end
-
def add_import_job
if forked?
job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
@@ -358,7 +452,7 @@ class Project < ActiveRecord::Base
end
end
- def clear_import_data
+ def reset_cache_and_import_attrs
update(import_error: nil)
ProjectCacheWorker.perform_async(self.id)
@@ -367,14 +461,16 @@ class Project < ActiveRecord::Base
end
def import_url=(value)
- import_url = Gitlab::ImportUrl.new(value)
+ return super(value) unless Gitlab::UrlSanitizer.valid?(value)
+
+ import_url = Gitlab::UrlSanitizer.new(value)
create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url)
end
def import_url
if import_data && super
- import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials)
+ import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
import_url.full_url
else
super
@@ -396,7 +492,7 @@ class Project < ActiveRecord::Base
end
def import?
- external_import? || forked?
+ external_import? || forked? || gitlab_project_import?
end
def no_import?
@@ -424,17 +520,22 @@ class Project < ActiveRecord::Base
end
def safe_import_url
- result = URI.parse(self.import_url)
- result.password = '*****' unless result.password.nil?
- result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
- result.to_s
- rescue
- self.import_url
+ Gitlab::UrlSanitizer.new(import_url).masked_url
+ end
+
+ def gitlab_project_import?
+ import_type == 'gitlab_project'
end
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
- self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
+ projects_limit = creator.projects_limit
+
+ if projects_limit == 0
+ self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
+ else
+ self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
+ end
end
rescue
self.errors.add(:base, "Can't check your ability to create project")
@@ -455,6 +556,16 @@ class Project < ActiveRecord::Base
self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
end
+ def check_wiki_path_conflict
+ return if path.blank?
+
+ path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki"
+
+ if Project.where(namespace_id: namespace_id, path: path_to_check).exists?
+ errors.add(:name, 'has already been taken')
+ end
+ end
+
def to_param
path
end
@@ -516,13 +627,21 @@ class Project < ActiveRecord::Base
end
def external_issue_tracker
- return @external_issue_tracker if defined?(@external_issue_tracker)
- @external_issue_tracker ||=
- services.issue_trackers.active.without_defaults.first
+ if has_external_issue_tracker.nil? # To populate existing projects
+ cache_has_external_issue_tracker
+ end
+
+ if has_external_issue_tracker?
+ return @external_issue_tracker if defined?(@external_issue_tracker)
+
+ @external_issue_tracker = services.external_issue_trackers.first
+ else
+ nil
+ end
end
- def can_have_issues_tracker_id?
- self.issues_enabled && !self.default_issues_tracker?
+ def cache_has_external_issue_tracker
+ update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
def build_missing_services
@@ -582,7 +701,7 @@ class Project < ActiveRecord::Base
end
def avatar_url
- if avatar.present?
+ if self[:avatar].present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self)
@@ -617,16 +736,6 @@ class Project < ActiveRecord::Base
end
end
- def project_member_by_name_or_email(name = nil, email = nil)
- user = users.find_by('name like ? or email like ?', name, email)
- project_members.where(user: user) if user
- end
-
- # Get Team Member record by user id
- def project_member_by_id(user_id)
- project_members.find_by(user_id: user_id)
- end
-
def name_with_namespace
@name_with_namespace ||= begin
if namespace
@@ -636,6 +745,7 @@ class Project < ActiveRecord::Base
end
end
end
+ alias_method :human_name, :name_with_namespace
def path_with_namespace
if namespace
@@ -692,18 +802,12 @@ class Project < ActiveRecord::Base
@repo_exists = false
end
+ # Branches that are not _exactly_ matched by a protected branch.
def open_branches
- # We're using a Set here as checking values in a large Set is faster than
- # checking values in a large Array.
- protected_set = Set.new(protected_branch_names)
-
- repository.branches.reject do |branch|
- protected_set.include?(branch.name)
- end
- end
-
- def protected_branch_names
- @protected_branch_names ||= protected_branches.pluck(:name)
+ exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name)
+ branch_names = repository.branches.map(&:name)
+ non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names))
+ repository.branches.reject { |branch| non_open_branch_names.include? branch.name }
end
def root_ref?(branch)
@@ -720,11 +824,12 @@ class Project < ActiveRecord::Base
# Check if current branch name is marked as protected in the system
def protected_branch?(branch_name)
- protected_branch_names.include?(branch_name)
+ @protected_branches ||= self.protected_branches.to_a
+ ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
end
def developers_can_push_to_protected_branch?(branch_name)
- protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push }
+ protected_branches.matching(branch_name).any?(&:developers_can_push)
end
def forked?
@@ -742,12 +847,17 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace)
- if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
+ if has_container_registry_tags?
+ # we currently doesn't support renaming repository if it contains tags in container registry
+ raise Exception.new('Project cannot be renamed, because tags are present in its container registry')
+ end
+
+ if gitlab_shell.mv_repository(repository_storage_path, 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
# So we basically we mute exceptions in next actions
begin
- gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
+ gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
@@ -888,7 +998,7 @@ class Project < ActiveRecord::Base
def create_repository
# Forked import is handled asynchronously
unless forked?
- if gitlab_shell.add_repository(path_with_namespace)
+ if gitlab_shell.add_repository(repository_storage_path, path_with_namespace)
repository.after_create
true
else
@@ -918,12 +1028,12 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
- def ci_commit(sha, ref)
- ci_commits.order(id: :desc).find_by(sha: sha, ref: ref)
+ def pipeline(sha, ref)
+ pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_ci_commit(sha, ref)
- ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref)
+ def ensure_pipeline(sha, ref)
+ pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref)
end
def enable_ci
@@ -938,13 +1048,13 @@ class Project < ActiveRecord::Base
shared_runners_enabled? && Ci::Runner.shared.active.any?(&block)
end
- def valid_runners_token? token
+ def valid_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
+ def valid_build_token?(token)
self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
@@ -999,4 +1109,49 @@ class Project < ActiveRecord::Base
update_attribute(:pending_delete, true)
end
+
+ def running_or_pending_build_count(force: false)
+ Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
+ builds.running_or_pending.count(:all)
+ end
+ end
+
+ def mark_import_as_failed(error_message)
+ original_errors = errors.dup
+ sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
+
+ import_fail
+ update_column(:import_error, sanitized_message)
+ rescue ActiveRecord::ActiveRecordError => e
+ Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
+ ensure
+ @errors = original_errors
+ end
+
+ def add_export_job(current_user:)
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
+
+ if job_id
+ Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
+ else
+ Rails.logger.error "Export job failed to start for project ID #{self.id}"
+ end
+ end
+
+ def export_path
+ File.join(Gitlab::ImportExport.storage_path, path_with_namespace)
+ end
+
+ def export_project_path
+ Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
+ end
+
+ def remove_exports
+ _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
+ status.zero?
+ end
+
+ def ensure_dir_exist
+ gitlab_shell.add_namespace(repository_storage_path, namespace.path)
+ end
end
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index e2f9ffb69ac..331123a5a5b 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -6,7 +6,9 @@ class ProjectImportData < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base,
marshal: true,
encode: true,
- mode: :per_attribute_iv_and_salt
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
serialize :data, JSON
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 1d1780dcfbf..b5c76e4d4fe 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,6 +1,4 @@
class BambooService < CiService
- include HTTParty
-
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -61,18 +59,7 @@ class BambooService < CiService
end
def build_info(sha)
- url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
-
- if username.blank? && password.blank?
- @response = HTTParty.get(url, verify: false)
- else
- url << '&os_authType=basic'
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
- end
+ @response = get_path("rest/api/latest/result?label=#{sha}")
end
def build_page(sha, ref)
@@ -80,11 +67,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
- URI.join(bamboo_url, "/browse/#{build_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key']
- URI.join(bamboo_url, "/browse/#{result_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
@@ -112,8 +99,27 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- # Bamboo requires a GET and does not take any data.
- url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
- self.class.get(url, verify: false)
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ private
+
+ def build_url(path)
+ URI.join("#{bamboo_url}/", path).to_s
+ end
+
+ def get_path(path)
+ url = build_url(path)
+
+ if username.blank? && password.blank?
+ HTTParty.get(url, verify: false)
+ else
+ url << '&os_authType=basic'
+ HTTParty.get(url, verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
new file mode 100644
index 00000000000..81af55aa29a
--- /dev/null
+++ b/app/models/project_services/bugzilla_service.rb
@@ -0,0 +1,23 @@
+class BugzillaService < IssueTrackerService
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+
+ def title
+ if self.properties && self.properties['title'].present?
+ self.properties['title']
+ else
+ 'Bugzilla'
+ end
+ end
+
+ def description
+ if self.properties && self.properties['description'].present?
+ self.properties['description']
+ else
+ 'Bugzilla issue tracker'
+ end
+ end
+
+ def to_param
+ 'bugzilla'
+ end
+end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 6b2b1daa724..63a5ed14484 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,5 +1,4 @@
class CustomIssueTrackerService < IssueTrackerService
-
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
@@ -31,8 +30,4 @@ class CustomIssueTrackerService < IssueTrackerService
{ type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
]
end
-
- def initialize_properties
- self.properties = {} if properties.nil?
- end
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 966dbc41d73..5e4dd101c53 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,5 +1,4 @@
class DroneCiService < CiService
-
prop_accessor :drone_url, :token, :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index f042bfc5210..d7b6e505191 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -25,7 +25,7 @@ class ExternalWikiService < Service
def execute(_data)
@response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil
- if @response !=200
+ if @response != 200
nil
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 0ff4f4c8dd2..23e5b16221b 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -106,7 +106,7 @@ class HipchatService < Service
else
message << "pushed to #{ref_type} <a href=\""\
"#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
- message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</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>)"
push[:commits].take(MAX_COMMITS).each do |commit|
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 91015e6c9b1..58cb720c3c1 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -70,7 +70,7 @@ class IrkerService < Service
private
def get_channels
- return true unless :activated?
+ return true unless activated?
return true if recipients.nil? || recipients.empty?
map_recipients
@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient)
end
- channels.reject! &:nil?
+ channels.reject!(&:nil?)
end
def format_channel(recipient)
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 6ae9b16d3ce..d1df6d0292f 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,5 +1,4 @@
class IssueTrackerService < Service
-
validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
default_value_for :category, 'issue_tracker'
@@ -38,9 +37,9 @@ class IssueTrackerService < Service
if enabled_in_gitlab_config
self.properties = {
title: issues_tracker['title'],
- project_url: add_issues_tracker_id(issues_tracker['project_url']),
- issues_url: add_issues_tracker_id(issues_tracker['issues_url']),
- new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url'])
+ project_url: issues_tracker['project_url'],
+ issues_url: issues_tracker['issues_url'],
+ new_issue_url: issues_tracker['new_issue_url']
}
else
self.properties = {}
@@ -83,16 +82,4 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
-
- def add_issues_tracker_id(url)
- if self.project
- id = self.project.issues_tracker_id
-
- if id
- url = url.gsub(":issues_tracker_id", id)
- end
- end
-
- url
- end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index beda89d3963..97bcbacf2b9 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -124,7 +124,7 @@ class JiraService < IssueTrackerService
def build_api_url_from_project_url
server = URI(project_url)
- default_ports = [["http",80],["https",443]].include?([server.scheme,server.port])
+ default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port])
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
"#{server_url}/rest/api/#{DEFAULT_API_VERSION}"
@@ -144,7 +144,7 @@ class JiraService < IssueTrackerService
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
- entity.last_commit.id
+ entity.diff_head_sha
end
commit_url = build_entity_url(:commit, commit_id)
@@ -190,7 +190,6 @@ class JiraService < IssueTrackerService
end
end
-
def auth
require 'base64'
Base64.urlsafe_encode64("#{self.username}:#{self.password}")
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index 11cce3e0561..f634e0772c0 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -1,5 +1,4 @@
class RedmineService < IssueTrackerService
-
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
index c124cad4afd..69c21b3fc38 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/slack_service/build_message.rb
@@ -35,8 +35,8 @@ class SlackService
private
def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)"
- end
+ "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
index 438ff33fdff..88e053ec192 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/slack_service/issue_message.rb
@@ -34,7 +34,12 @@ class SlackService
private
def message
- "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*"
+ case state
+ when "opened"
+ "[#{project_link}] Issue #{state} by #{user_name}"
+ else
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
+ end
end
def opened_issue?
@@ -42,7 +47,11 @@ class SlackService
end
def description_message
- [{ text: format(description), color: attachment_color }]
+ [{
+ title: issue_title,
+ title_link: issue_url,
+ text: format(description),
+ color: "#C95823" }]
end
def project_link
@@ -50,7 +59,11 @@ class SlackService
end
def issue_link
- "[issue ##{issue_iid}](#{issue_url})"
+ "[#{issue_title}](#{issue_url})"
+ end
+
+ def issue_title
+ "##{issue_iid} #{title}"
end
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b0dcb52eba1..a4a967c9bc9 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,6 +1,4 @@
class TeamcityService < CiService
- include HTTParty
-
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -64,15 +62,7 @@ class TeamcityService < CiService
end
def build_info(sha)
- url = URI.join(
- teamcity_url,
- "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
- ).to_s
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
+ @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
end
def build_page(sha, ref)
@@ -81,14 +71,11 @@ class TeamcityService < CiService
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
- URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
+ build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
- URI.join(
- teamcity_url,
- "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
- ).to_s
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
@@ -123,8 +110,8 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
- self.class.post(
- URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
+ HTTParty.post(
+ build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
'</build>',
@@ -132,4 +119,18 @@ class TeamcityService < CiService
basic_auth: auth
)
end
+
+ private
+
+ def build_url(path)
+ URI.join("#{teamcity_url}/", path).to_s
+ end
+
+ def get_path(path)
+ HTTParty.get(build_url(path), verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 5fba6baa204..25b5d777641 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -7,5 +7,6 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
- participant :author, :notes
+ participant :author
+ participant :notes_with_associations
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 70a8bbaba65..0b700930641 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,23 +21,13 @@ class ProjectTeam
end
end
- def find(user_id)
- user = project.users.find_by(id: user_id)
-
- if group
- user ||= group.users.find_by(id: user_id)
- end
-
- user
- end
-
def find_member(user_id)
- member = project.project_members.find_by(user_id: user_id)
+ member = project.members.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
if group && !member
- member = group.group_members.find_by(user_id: user_id)
+ member = group.members.find_by(user_id: user_id)
end
member
@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project)
end
- def users
- members
- end
-
def members
@members ||= fetch_members
end
+ alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
@@ -131,8 +118,14 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user_id)
- !!find_member(user_id)
+ def member?(user, min_member_access = nil)
+ member = !!find_member(user.id)
+
+ if min_member_access
+ member && max_member_access(user.id) >= min_member_access
+ else
+ member
+ end
end
def human_max_access(user_id)
@@ -144,20 +137,10 @@ class ProjectTeam
def max_member_access(user_id)
access = []
- project.project_members.each do |member|
- if member.user_id == user_id
- access << member.access_field if member.access_field
- break
- end
- end
+ access += project.members.where(user_id: user_id).has_access.pluck(:access_level)
if group
- group.group_members.each do |member|
- if member.user_id == user_id
- access << member.access_field if member.access_field
- break
- end
- end
+ access += group.members.where(user_id: user_id).has_access.pluck(:access_level)
end
if project.invited_groups.any? && project.allowed_to_share_with_group?
@@ -167,6 +150,7 @@ class ProjectTeam
access.compact.max
end
+ private
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
@@ -183,17 +167,15 @@ class ProjectTeam
end.compact.max
end
- private
-
def fetch_members(level = nil)
- project_members = project.project_members
- group_members = group ? group.group_members : []
+ project_members = project.members
+ group_members = 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
+ im = invited_group.members
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 7c1a61bb0bf..a255710f577 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -27,6 +27,10 @@ class ProjectWiki
@project.path_with_namespace + ".wiki"
end
+ def web_url
+ Gitlab::Routing.url_helpers.namespace_project_wiki_url(@project.namespace, @project, :home)
+ end
+
def url_to_repo
gitlab_shell.url_to_repo(path_with_namespace)
end
@@ -40,7 +44,7 @@ class ProjectWiki
end
def wiki_base_path
- ["/", @project.path_with_namespace, "/wikis"].join('')
+ [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('')
end
# Returns the Gollum::Wiki object.
@@ -113,7 +117,7 @@ class ProjectWiki
end
def page_title_and_dir(title)
- title_array = title.split("/")
+ title_array = title.split("/")
title = title_array.pop
[title, title_array.join("/")]
end
@@ -142,10 +146,20 @@ class ProjectWiki
wiki
end
+ def hook_attrs
+ {
+ web_url: web_url,
+ git_ssh_url: ssh_url_to_repo,
+ git_http_url: http_url_to_repo,
+ path_with_namespace: path_with_namespace,
+ default_branch: default_branch
+ }
+ end
+
private
def init_repo(path_with_namespace)
- gitlab_shell.add_repository(path_with_namespace)
+ gitlab_shell.add_repository(project.repository_storage_path, path_with_namespace)
end
def commit_details(action, message = nil, title = nil)
@@ -159,7 +173,7 @@ class ProjectWiki
end
def path_to_repo
- @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git")
+ @path_to_repo ||= File.join(project.repository_storage_path, "#{path_with_namespace}.git")
end
def update_project_activity
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 33cf046fa75..b7011d7afdf 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -8,4 +8,51 @@ class ProtectedBranch < ActiveRecord::Base
def commit
project.commit(self.name)
end
+
+ # Returns all protected branches that match the given branch name.
+ # This realizes all records from the scope built up so far, and does
+ # _not_ return a relation.
+ #
+ # This method optionally takes in a list of `protected_branches` to search
+ # through, to avoid calling out to the database.
+ def self.matching(branch_name, protected_branches: nil)
+ (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) }
+ end
+
+ # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`])
+ # that match the current protected branch.
+ def matching(branches)
+ branches.select { |branch| self.matches?(branch.name) }
+ end
+
+ # Checks if the protected branch matches the given branch name.
+ def matches?(branch_name)
+ return false if self.name.blank?
+
+ exact_match?(branch_name) || wildcard_match?(branch_name)
+ end
+
+ # Checks if this protected branch contains a wildcard
+ def wildcard?
+ self.name && self.name.include?('*')
+ end
+
+ protected
+
+ def exact_match?(branch_name)
+ self.name == branch_name
+ end
+
+ def wildcard_match?(branch_name)
+ wildcard_regex === branch_name
+ end
+
+ def wildcard_regex
+ @wildcard_regex ||= begin
+ name = self.name.gsub('*', 'STAR_DONT_ESCAPE')
+ quoted_name = Regexp.quote(name)
+ regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
+ /\A#{regex_string}\z/
+ end
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a4b42d7226d..5b670cb4b8f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,7 +39,7 @@ class Repository
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
- File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git")
+ File.join(@project.repository_storage_path, path_with_namespace + ".git")
)
end
@@ -78,9 +78,9 @@ class Repository
end
end
- def commit(id = 'HEAD')
+ def commit(ref = 'HEAD')
return nil unless exists?
- commit = Gitlab::Git::Commit.find(raw_repository, id)
+ commit = Gitlab::Git::Commit.find(raw_repository, ref)
commit = ::Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError
@@ -130,7 +130,7 @@ class Repository
end
def find_tag(name)
- raw_repository.tags.find { |tag| tag.name == name }
+ tags.find { |tag| tag.name == name }
end
def add_branch(user, branch_name, target)
@@ -191,8 +191,36 @@ class Repository
end
end
+ def ref_names
+ branch_names + tag_names
+ end
+
def branch_names
- cache.fetch(:branch_names) { branches.map(&:name) }
+ @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
+ end
+
+ def branch_exists?(branch_name)
+ branch_names.include?(branch_name)
+ end
+
+ def ref_exists?(ref)
+ rugged.references.exist?(ref)
+ end
+
+ # Makes sure a commit is kept around when Git garbage collection runs.
+ # Git GC will delete commits from the repository that are no longer in any
+ # branches or tags, but we want to keep some of these commits around, for
+ # example if they have comments or CI builds.
+ def keep_around(sha)
+ return unless sha && commit(sha)
+
+ return if kept_around?(sha)
+
+ rugged.references.create(keep_around_ref_name(sha), sha)
+ end
+
+ def kept_around?(sha)
+ ref_exists?(keep_around_ref_name(sha))
end
def tag_names
@@ -238,14 +266,20 @@ class Repository
end
end
+ # Keys for data that can be affected for any commit push.
def cache_keys
- %i(size branch_names tag_names commit_count
+ %i(size commit_count
readme version contribution_guide changelog
- license_blob license_key)
+ license_blob license_key gitignore)
+ end
+
+ # Keys for data on branch/tag operations.
+ def cache_keys_for_branches_and_tags
+ %i(branch_names tag_names branch_count tag_count)
end
def build_cache
- cache_keys.each do |key|
+ (cache_keys + cache_keys_for_branches_and_tags).each do |key|
unless cache.exist?(key)
send(key)
end
@@ -259,6 +293,7 @@ class Repository
def expire_branches_cache
cache.expire(:branch_names)
+ @branch_names = nil
@local_branches = nil
end
@@ -273,8 +308,6 @@ class Repository
# This ensures this particular cache is flushed after the first commit to a
# new repository.
expire_emptiness_caches if empty?
- expire_branch_count_cache
- expire_tag_count_cache
end
def expire_branch_cache(branch_name = nil)
@@ -324,10 +357,6 @@ class Repository
@lookup_cache ||= {}
end
- def expire_branch_names
- 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.
@@ -438,7 +467,7 @@ class Repository
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
- Gitlab::Git::Blob.find(self, sha, path)
+ Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
end
@@ -453,7 +482,7 @@ class Repository
def version
cache.fetch(:version) do
tree(:head).blobs.find do |file|
- file.name.downcase == 'version'
+ file.name.casecmp('version').zero?
end
end
end
@@ -468,33 +497,37 @@ class Repository
def changelog
cache.fetch(:changelog) do
- tree(:head).blobs.find do |file|
- file.name =~ /\A(changelog|history|changes|news)/i
- end
+ file_on_head(/\A(changelog|history|changes|news)/i)
end
end
def license_blob
- return nil if !exists? || empty?
+ return nil unless head_exists?
cache.fetch(:license_blob) do
- tree(:head).blobs.find do |file|
- file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i
- end
+ file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
end
end
def license_key
- return nil if !exists? || empty?
+ return nil unless head_exists?
cache.fetch(:license_key) do
Licensee.license(path).try(:key)
end
end
- def gitlab_ci_yml
+ def gitignore
return nil if !exists? || empty?
+ cache.fetch(:gitignore) do
+ file_on_head(/\A\.gitignore\z/)
+ end
+ end
+
+ def gitlab_ci_yml
+ return nil unless head_exists?
+
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
@@ -586,6 +619,21 @@ class Repository
end
end
+ def tags_sorted_by(value)
+ case value
+ when 'name'
+ # Would be better to use `sort_by` but `version_sorter` only exposes
+ # `sort` and `rsort`
+ VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
+ when 'updated_desc'
+ tags_sorted_by_committed_date.reverse
+ when 'updated_asc'
+ tags_sorted_by_committed_date
+ else
+ tags
+ end
+ end
+
def contributors
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
@@ -605,16 +653,6 @@ class Repository
end
end
- def blob_for_diff(commit, diff)
- blob_at(commit.id, diff.file_path)
- end
-
- def prev_blob_for_diff(commit, diff)
- if commit.parent_id
- blob_at(commit.parent_id, diff.old_path)
- end
- end
-
def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
@@ -795,7 +833,7 @@ class Repository
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?
+ args << { mainline: 1 } if commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
@@ -809,7 +847,7 @@ class Repository
def check_cherry_pick_content(commit, base_branch)
source_sha = find_branch(base_branch).target
args = [commit.id, source_sha]
- args << 1 if commit.merge_commit?
+ args << 1 if commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
@@ -847,10 +885,9 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
-
def search_files(query, ref)
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
@@ -864,7 +901,7 @@ class Repository
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
- extname = File.extname(filename)
+ extname = Regexp.escape(File.extname(filename))
basename = filename.sub(/#{extname}$/, '')
break
end
@@ -950,6 +987,10 @@ class Repository
raw_repository.ls_files(actual_ref)
end
+ def gitattribute(path, name)
+ raw_repository.attributes(path)[name]
+ end
+
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
@@ -960,12 +1001,6 @@ class Repository
end
end
- def main_language
- return if empty? || rugged.head_unborn?
-
- Linguist::Repository.new(rugged, rugged.head.target_id).language
- end
-
def avatar
return nil unless exists?
@@ -981,4 +1016,20 @@ class Repository
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
+
+ def head_exists?
+ exists? && !empty? && !rugged.head_unborn?
+ end
+
+ def file_on_head(regex)
+ tree(:head).blobs.find { |file| file.name =~ regex }
+ end
+
+ def tags_sorted_by_committed_date
+ tags.sort_by { |tag| commit(tag.target).committed_date }
+ end
+
+ def keep_around_ref_name(sha)
+ "refs/keep-around/#{sha}"
+ end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 375f195dba7..016172c6d7e 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -1,4 +1,6 @@
class SentNotification < ActiveRecord::Base
+ serialize :position, Gitlab::Diff::Position
+
belongs_to :project
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
@@ -7,7 +9,9 @@ class SentNotification < ActiveRecord::Base
validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- validates :line_code, line_code: true, allow_blank: true
+ validate :note_valid
+
+ after_save :keep_around_commit
class << self
def reply_key
@@ -18,7 +22,7 @@ class SentNotification < ActiveRecord::Base
find_by(reply_key: reply_key)
end
- def record(noteable, recipient_id, reply_key, params = {})
+ def record(noteable, recipient_id, reply_key, attrs = {})
return unless reply_key
noteable_id = nil
@@ -29,7 +33,7 @@ class SentNotification < ActiveRecord::Base
noteable_id = noteable.id
end
- params.reverse_merge!(
+ attrs.reverse_merge!(
project: noteable.project,
noteable_type: noteable.class.name,
noteable_id: noteable_id,
@@ -38,13 +42,17 @@ class SentNotification < ActiveRecord::Base
reply_key: reply_key
)
- create(params)
+ create(attrs)
end
- def record_note(note, recipient_id, reply_key, params = {})
- params[:line_code] = note.line_code
+ def record_note(note, recipient_id, reply_key, attrs = {})
+ if note.diff_note?
+ attrs[:note_type] = note.type
+
+ attrs.merge!(note.diff_attributes)
+ end
- record(note.noteable, recipient_id, reply_key, params)
+ record(note.noteable, recipient_id, reply_key, attrs)
end
end
@@ -67,4 +75,35 @@ class SentNotification < ActiveRecord::Base
def to_param
self.reply_key
end
+
+ def note_attributes
+ {
+ project: self.project,
+ author: self.recipient,
+ type: self.note_type,
+ noteable_type: self.noteable_type,
+ noteable_id: self.noteable_id,
+ commit_id: self.commit_id,
+ line_code: self.line_code,
+ position: self.position.to_json
+ }
+ end
+
+ def create_note(note)
+ Notes::CreateService.new(
+ self.project,
+ self.recipient,
+ self.note_attributes.merge(note: note)
+ ).execute
+ end
+
+ private
+
+ def note_valid
+ Note.new(note_attributes.merge(note: "Test")).valid?
+ end
+
+ def keep_around_commit
+ project.repository.keep_around(self.commit_id)
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index de3fd24584a..d7a32c28267 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -16,8 +16,9 @@ class Service < ActiveRecord::Base
after_initialize :initialize_properties
after_commit :reset_updated_properties
+ after_commit :cache_project_has_external_issue_tracker
- belongs_to :project
+ belongs_to :project, inverse_of: :services
has_one :service_hook
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
@@ -34,6 +35,7 @@ class Service < ActiveRecord::Base
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
+ scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
default_value_for :category, 'common'
@@ -168,6 +170,7 @@ class Service < ActiveRecord::Base
bamboo
buildkite
builds_email
+ bugzilla
campfire
custom_issue_tracker
drone_ci
@@ -192,4 +195,12 @@ class Service < ActiveRecord::Base
service.project_id = project_id
service if service.save
end
+
+ private
+
+ def cache_project_has_external_issue_tracker
+ if project && !project.destroyed?
+ project.cache_has_external_issue_tracker
+ end
+ end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 0a3c3b57669..5ec933601ac 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -20,6 +20,7 @@ class Snippet < ActiveRecord::Base
length: { within: 0..255 },
format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message }
+
validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
@@ -30,7 +31,8 @@ class Snippet < ActiveRecord::Base
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
- participant :author, :notes
+ participant :author
+ participant :notes_with_associations
def self.reference_prefix
'$'
@@ -80,6 +82,11 @@ class Snippet < ActiveRecord::Base
0
end
+ # alias for compatibility with blobs and highlighting
+ def path
+ file_name
+ end
+
def name
file_name
end
@@ -100,6 +107,10 @@ class Snippet < ActiveRecord::Base
content.lines.count > 1000
end
+ def notes_with_associations
+ notes.includes(:author)
+ end
+
class << self
# Searches for snippets with a matching title or file name.
#
@@ -130,7 +141,16 @@ class Snippet < ActiveRecord::Base
end
def accessible_to(user)
- where('visibility_level IN (?) OR author_id = ?', [Snippet::INTERNAL, Snippet::PUBLIC], user)
+ return are_public unless user.present?
+ return all if user.admin?
+
+ where(
+ 'visibility_level IN (:visibility_levels)
+ OR author_id = :author_id
+ OR project_id IN (:project_ids)',
+ visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL],
+ author_id: user.id,
+ project_ids: user.authorized_projects.select(:id))
end
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f8b59fe4126..ac3fdbc7f3b 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,6 +1,15 @@
class Todo < ActiveRecord::Base
- ASSIGNED = 1
- MENTIONED = 2
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
+ MARKED = 4
+
+ ACTION_NAMES = {
+ ASSIGNED => :assigned,
+ MENTIONED => :mentioned,
+ BUILD_FAILED => :build_failed,
+ MARKED => :marked
+ }
belongs_to :author, class_name: "User"
belongs_to :note
@@ -28,6 +37,16 @@ class Todo < ActiveRecord::Base
state :done
end
+ after_save :keep_around_commit
+
+ def build_failed?
+ action == BUILD_FAILED
+ end
+
+ def action_name
+ ACTION_NAMES[action]
+ end
+
def body
if note.present?
note.note
@@ -56,4 +75,10 @@ class Todo < ActiveRecord::Base
target.to_reference
end
end
+
+ private
+
+ def keep_around_commit
+ project.repository.keep_around(self.commit_id)
+ end
end
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
new file mode 100644
index 00000000000..00b19686d48
--- /dev/null
+++ b/app/models/u2f_registration.rb
@@ -0,0 +1,40 @@
+# Registration information for U2F (universal 2nd factor) devices, like Yubikeys
+
+class U2fRegistration < ActiveRecord::Base
+ belongs_to :user
+
+ def self.register(user, app_id, json_response, challenges)
+ u2f = U2F::U2F.new(app_id)
+ registration = self.new
+
+ begin
+ response = U2F::RegisterResponse.load_from_json(json_response)
+ registration_data = u2f.register!(challenges, response)
+ registration.update(certificate: registration_data.certificate,
+ key_handle: registration_data.key_handle,
+ public_key: registration_data.public_key,
+ counter: registration_data.counter,
+ user: user)
+ rescue JSON::ParserError, NoMethodError, ArgumentError
+ registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
+ rescue U2F::Error => e
+ registration.errors.add(:base, e.message)
+ end
+
+ registration
+ end
+
+ def self.authenticate(user, app_id, json_response, challenges)
+ response = U2F::SignResponse.load_from_json(json_response)
+ registration = user.u2f_registrations.find_by_key_handle(response.key_handle)
+ u2f = U2F::U2F.new(app_id)
+
+ if registration
+ u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter)
+ registration.update(counter: response.counter)
+ true
+ end
+ rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
+ false
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1e4814641d1..79c670cb35a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -10,24 +10,31 @@ class User < ActiveRecord::Base
include CaseSensitivity
include TokenAuthenticatable
+ DEFAULT_NOTIFICATION_LEVEL = :participating
+
add_authentication_token_field :authentication_token
default_value_for :admin, false
- default_value_for :external, false
+ default_value_for(:external) { current_application_settings.user_default_external }
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
default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
+ attr_encrypted :otp_secret,
+ key: Gitlab::Application.config.secret_key_base,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
+
devise :two_factor_authenticatable,
otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
- alias_attribute :two_factor_enabled, :otp_required_for_login
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
- devise :lockable, :async, :recoverable, :rememberable, :trackable,
+ devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
attr_accessor :force_random_password
@@ -45,12 +52,13 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
+ has_many :u2f_registrations, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
- has_many :project_members, source: 'ProjectMember'
- has_many :group_members, source: 'GroupMember'
+ has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
@@ -58,13 +66,13 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
+ has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember'
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
- has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
@@ -79,6 +87,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
+ has_many :award_emoji, as: :awardable, dependent: :destroy
#
# Validations
@@ -93,7 +102,6 @@ class User < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- validates :notification_level, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
@@ -112,6 +120,7 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
+ before_create :check_confirmation_email
after_create :post_create_hook
after_destroy :post_destroy_hook
@@ -126,13 +135,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
- # Notification level
- # Note: When adding an option, it MUST go on the end of the array.
- #
- # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
- # Because user.notification_disabled? is much better than user.disabled?
- enum notification_level: [:disabled, :participating, :watch, :global, :mention]
-
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -168,8 +170,16 @@ class User < ActiveRecord::Base
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)') }
- scope :with_two_factor, -> { where(two_factor_enabled: true) }
- scope :without_two_factor, -> { where(two_factor_enabled: false) }
+
+ def self.with_two_factor
+ joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
+ where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
+ end
+
+ def self.without_two_factor
+ joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
+ where("u2f.id IS NULL AND otp_required_for_login = ?", false)
+ end
#
# Class methods
@@ -259,6 +269,11 @@ class User < ActiveRecord::Base
find_by!('lower(username) = ?', username.downcase)
end
+ def find_by_personal_access_token(token_string)
+ personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
+ personal_access_token.user if personal_access_token
+ end
+
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
@@ -294,7 +309,7 @@ class User < ActiveRecord::Base
def generate_password
if self.force_random_password
- self.password = self.password_confirmation = Devise.friendly_token.first(8)
+ self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end
@@ -307,19 +322,38 @@ class User < ActiveRecord::Base
@reset_token
end
+ def check_confirmation_email
+ skip_confirmation! unless current_application_settings.send_user_confirmation_email
+ end
+
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
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_grace_period_started_at: nil,
- otp_backup_codes: nil
- )
+ transaction do
+ update_attributes(
+ otp_required_for_login: 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
+ )
+ self.u2f_registrations.destroy_all
+ end
+ end
+
+ def two_factor_enabled?
+ two_factor_otp_enabled? || two_factor_u2f_enabled?
+ end
+
+ def two_factor_otp_enabled?
+ self.otp_required_for_login?
+ end
+
+ def two_factor_u2f_enabled?
+ self.u2f_registrations.exists?
end
def namespace_uniq
@@ -377,8 +411,13 @@ class User < ActiveRecord::Base
end
# Returns projects user is authorized to access.
- def authorized_projects
- Project.where("projects.id IN (#{projects_union.to_sql})")
+ def authorized_projects(min_access_level = nil)
+ Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
+ end
+
+ def viewable_starred_projects
+ starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
+ [Project::PUBLIC, Project::INTERNAL])
end
def owned_projects
@@ -387,11 +426,6 @@ class User < ActiveRecord::Base
owned_groups.select(:id), namespace.id).joins(:namespace)
end
- # Team membership in authorized projects
- def tm_in_authorized_projects
- ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
- end
-
def is_admin?
admin
end
@@ -454,9 +488,8 @@ class User < ActiveRecord::Base
events.recent.find do |event|
project = Project.find_by_id(event.project_id)
next unless project
- repo = project.repository
- if repo.branch_names.include?(event.branch_name)
+ if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
where(source_project_id: project.id,
source_branch: event.branch_name)
@@ -481,10 +514,6 @@ class User < ActiveRecord::Base
"#{name} (#{username})"
end
- def tm_of(project)
- project.project_member_by_id(self.id)
- end
-
def already_forked?(project)
!!fork_of(project)
end
@@ -624,7 +653,7 @@ class User < ActiveRecord::Base
end
def avatar_url(size = nil, scale = 2)
- if avatar.present?
+ if self[:avatar].present?
[gitlab_config.url, avatar.url].join
else
GravatarService.new.execute(email, size, scale)
@@ -735,7 +764,7 @@ class User < ActiveRecord::Base
unless email_domains.blank?
match_found = email_domains.any? do |domain|
- escaped = Regexp.escape(domain).gsub('\*','.*?')
+ escaped = Regexp.escape(domain).gsub('\*', '.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
email_domain = Mail::Address.new(self.email).domain
email_domain =~ regexp
@@ -770,13 +799,65 @@ class User < ActiveRecord::Base
notification_settings.find_or_initialize_by(source: source)
end
+ # Lazy load global notification setting
+ # Initializes User setting with Participating level if setting not persisted
+ def global_notification_setting
+ return @global_notification_setting if defined?(@global_notification_setting)
+
+ @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
+ @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
+
+ @global_notification_setting
+ end
+
+ def assigned_open_merge_request_count(force: false)
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
+ assigned_merge_requests.opened.count
+ end
+ end
+
+ def assigned_open_issues_count(force: false)
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
+ assigned_issues.opened.count
+ end
+ end
+
+ def update_cache_counts
+ assigned_open_merge_request_count(force: true)
+ assigned_open_issues_count(force: true)
+ end
+
+ def todos_done_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
+ todos.done.count
+ end
+ end
+
+ def todos_pending_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
+ todos.pending.count
+ end
+ end
+
+ def update_todos_count_cache
+ todos_done_count(force: true)
+ todos_pending_count(force: true)
+ end
+
private
- def projects_union
- Gitlab::SQL::Union.new([personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)])
+ def projects_union(min_access_level = nil)
+ relations = [personal_projects.select(:id),
+ groups_projects.select(:id),
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)]
+
+ if min_access_level
+ scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
+ relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
+ end
+
+ Gitlab::SQL::Union.new(relations)
end
def ci_projects_union
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index a7f090655e1..8a000585e89 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -7,7 +7,7 @@ class AuditEventService
@details = {
with: @details[:with],
target_id: @author.id,
- target_type: "User",
+ target_type: 'User',
target_details: @author.name,
}
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
new file mode 100644
index 00000000000..e294a962352
--- /dev/null
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -0,0 +1,89 @@
+module Auth
+ class ContainerRegistryAuthenticationService < BaseService
+ include Gitlab::CurrentSettings
+
+ AUDIENCE = 'container_registry'
+
+ def execute
+ return error('not found', 404) unless registry.enabled
+
+ unless current_user || project
+ return error('forbidden', 403) unless scope
+ end
+
+ { token: authorized_token(scope).encoded }
+ end
+
+ def self.full_access_token(*names)
+ registry = Gitlab.config.registry
+ token = JSONWebToken::RSAToken.new(registry.key)
+ token.issuer = registry.issuer
+ token.audience = AUDIENCE
+ token.expire_time = token_expire_at
+
+ token[:access] = names.map do |name|
+ { type: 'repository', name: name, actions: %w(*) }
+ end
+
+ token.encoded
+ end
+
+ private
+
+ def authorized_token(*accesses)
+ token = JSONWebToken::RSAToken.new(registry.key)
+ token.issuer = registry.issuer
+ token.audience = params[:service]
+ token.subject = current_user.try(:username)
+ token.expire_time = ContainerRegistryAuthenticationService.token_expire_at
+ token[:access] = accesses.compact
+ token
+ end
+
+ def scope
+ return unless params[:scope]
+
+ @scope ||= process_scope(params[:scope])
+ end
+
+ def process_scope(scope)
+ type, name, actions = scope.split(':', 3)
+ actions = actions.split(',')
+ return unless type == 'repository'
+
+ process_repository_access(type, name, actions)
+ end
+
+ def process_repository_access(type, name, actions)
+ requested_project = Project.find_with_namespace(name)
+ return unless requested_project
+
+ actions = actions.select do |action|
+ can_access?(requested_project, action)
+ end
+
+ { type: type, name: name, actions: actions } if actions.present?
+ end
+
+ def can_access?(requested_project, requested_action)
+ return false unless requested_project.container_registry_enabled?
+
+ case requested_action
+ when 'pull'
+ requested_project == project || can?(current_user, :read_container_image, requested_project)
+ when 'push'
+ requested_project == project || can?(current_user, :create_container_image, requested_project)
+ else
+ false
+ end
+ end
+
+ def registry
+ Gitlab.config.registry
+ end
+
+ def self.token_expire_at
+ Time.now + current_application_settings.container_registry_token_expire_delay.minutes
+ end
+ end
+end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 18274ce24e2..2dcb052d274 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -1,11 +1,12 @@
module Ci
class CreateBuildsService
- def initialize(commit)
- @commit = commit
+ def initialize(pipeline)
+ @pipeline = pipeline
+ @config = pipeline.config_processor
end
def execute(stage, user, status, trigger_request = nil)
- builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request)
+ builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
@@ -19,33 +20,37 @@ module Ci
end
end
+ # don't create the same build twice
+ builds_attrs.reject! do |build_attrs|
+ @pipeline.builds.find_by(ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ name: build_attrs[:name])
+ end
+
builds_attrs.map do |build_attrs|
- # don't create the same build twice
- unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag,
- trigger_request: trigger_request, name: build_attrs[:name])
- build_attrs.slice!(:name,
- :commands,
- :tag_list,
- :options,
- :allow_failure,
- :stage,
- :stage_idx)
+ build_attrs.slice!(:name,
+ :commands,
+ :tag_list,
+ :options,
+ :allow_failure,
+ :stage,
+ :stage_idx,
+ :environment)
- build_attrs.merge!(ref: @commit.ref,
- tag: @commit.tag,
- trigger_request: trigger_request,
- user: user,
- project: @commit.project)
+ build_attrs.merge!(pipeline: @pipeline,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ user: user,
+ project: @pipeline.project)
- @commit.builds.create!(build_attrs)
- end
+ ##
+ # We do not persist new builds here.
+ # Those will be persisted when @pipeline is saved.
+ #
+ @pipeline.builds.new(build_attrs)
end
end
-
- private
-
- def config_processor
- @config_processor ||= @commit.config_processor
- end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
new file mode 100644
index 00000000000..b1ee6874190
--- /dev/null
+++ b/app/services/ci/create_pipeline_service.rb
@@ -0,0 +1,48 @@
+module Ci
+ class CreatePipelineService < BaseService
+ def execute
+ pipeline = project.pipelines.new(params)
+
+ unless ref_names.include?(params[:ref])
+ pipeline.errors.add(:base, 'Reference not found')
+ return pipeline
+ end
+
+ if commit
+ pipeline.sha = commit.id
+ else
+ pipeline.errors.add(:base, 'Commit not found')
+ return pipeline
+ end
+
+ unless can?(current_user, :create_pipeline, project)
+ pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
+ return pipeline
+ end
+
+ unless pipeline.config_processor
+ pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
+ return pipeline
+ end
+
+ pipeline.save!
+
+ unless pipeline.create_builds(current_user)
+ pipeline.errors.add(:base, 'No builds for this pipeline.')
+ end
+
+ pipeline.save
+ pipeline
+ end
+
+ private
+
+ def ref_names
+ @ref_names ||= project.repository.ref_names
+ end
+
+ def commit
+ @commit ||= project.commit(params[:ref])
+ end
+ end
+end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 993acf11db9..1e629cf119a 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -7,14 +7,14 @@ module Ci
# check if ref is tag
tag = project.repository.find_tag(ref).present?
- ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag)
+ pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
trigger_request = trigger.trigger_requests.create!(
variables: variables,
- commit: ci_commit,
+ pipeline: pipeline,
)
- if ci_commit.create_builds(nil, trigger_request)
+ if pipeline.create_builds(nil, trigger_request)
trigger_request
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 3018f27ec05..75d847d5bee 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -3,9 +3,9 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
- ci_commits = project.ci_commits.where(sha: sha)
- ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(ci_commits.status)
+ pipelines = project.pipelines.where(sha: sha)
+ pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
+ image_name = image_for_status(pipelines.status)
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(path: image_path, name: image_name)
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 4ff268a6f06..9a187f5d694 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -7,17 +7,21 @@ module Ci
builds =
if current_runner.shared?
- # don't run projects which have not enables shared runners
- builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true })
+ builds.
+ # don't run projects which have not enabled shared runners
+ joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
+
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else
- # do run projects which are only assigned to this runner
- builds.where(project: current_runner.projects.where(builds_enabled: true))
+ # do run projects which are only assigned to this runner (FIFO)
+ builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
end
- builds = builds.order('created_at ASC')
-
build = builds.find do |build|
- build.can_be_served?(current_runner)
+ current_runner.can_pick?(build)
end
if build
@@ -35,5 +39,12 @@ module Ci
rescue StateMachines::InvalidTransition
nil
end
+
+ private
+
+ def running_builds_for_shared_runners
+ Ci::Build.running.where(runner: Ci::Runner.shared).
+ group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+ end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 6b69cb53b2c..c578097376a 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -23,7 +23,7 @@ module Commits
private
def check_push_permissions
- allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+ allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 9f4481a8153..d874582d54f 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -3,17 +3,20 @@ require_relative 'base_service'
class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
- if valid_branch == false
+
+ unless valid_branch
return error('Branch name is invalid')
end
repository = project.repository
existing_branch = repository.find_branch(branch_name)
+
if existing_branch
return error('Branch already exists')
end
new_branch = nil
+
if source_project != @project
repository.with_tmp_ref do |tmp_ref|
repository.fetch_ref(
@@ -29,18 +32,15 @@ class CreateBranchService < BaseService
end
if new_branch
- # GitPushService handles execution of services and hooks for branch pushes
success(new_branch)
else
error('Invalid reference name')
end
- rescue GitHooksService::PreReceiveError
- error('Branch creation was rejected by Git hook')
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
end
def success(branch)
- out = super()
- out[:branch] = branch
- out
+ super().merge(branch: branch)
end
end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 0d2aa1ff03d..f947e8f452e 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -1,15 +1,11 @@
class CreateCommitBuildsService
def execute(project, user, params)
- return false unless project.builds_enabled?
+ return unless project.builds_enabled?
before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
- unless origin_ref && sha.present?
- return false
- end
-
ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref)
@@ -18,26 +14,50 @@ class CreateCommitBuildsService
return false
end
- commit = project.ci_commit(sha, ref)
- unless commit
- commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
-
- # Skip creating ci_commit when no gitlab-ci.yml is found
- unless commit.ci_yaml_file
- return false
- end
+ @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
- # Create a new ci_commit
- commit.save!
+ ##
+ # Skip creating pipeline if no gitlab-ci.yml is found
+ #
+ unless @pipeline.ci_yaml_file
+ return false
end
+ ##
# Skip creating builds for commits that have [ci skip]
- unless commit.skip_ci?
- # Create builds for commit
- commit.create_builds(user)
+ # but save pipeline object
+ #
+ if @pipeline.skip_ci?
+ return save_pipeline!
+ end
+
+ ##
+ # Skip creating builds when CI config is invalid
+ # but save pipeline object
+ #
+ unless @pipeline.config_processor
+ return save_pipeline!
end
- commit.touch
- commit
+ ##
+ # Skip creating pipeline object if there are no builds for it.
+ #
+ unless @pipeline.create_builds(user)
+ @pipeline.errors.add(:base, 'No builds created')
+ return false
+ end
+
+ save_pipeline!
+ end
+
+ private
+
+ ##
+ # Create a new pipeline and touch object to calculate status
+ #
+ def save_pipeline!
+ @pipeline.save!
+ @pipeline.touch
+ @pipeline
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
new file mode 100644
index 00000000000..efeb9df9527
--- /dev/null
+++ b/app/services/create_deployment_service.rb
@@ -0,0 +1,18 @@
+require_relative 'base_service'
+
+class CreateDeploymentService < BaseService
+ def execute(deployable = nil)
+ environment = project.environments.find_or_create_by(
+ name: params[:environment]
+ )
+
+ project.deployments.create(
+ environment: environment,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ deployable: deployable
+ )
+ end
+end
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
index e06a6f2f47a..d6d4afcf29a 100644
--- a/app/services/create_release_service.rb
+++ b/app/services/create_release_service.rb
@@ -2,7 +2,6 @@ require_relative 'base_service'
class CreateReleaseService < BaseService
def execute(tag_name, release_description)
-
repository = project.repository
existing_tag = repository.find_tag(tag_name)
@@ -24,8 +23,6 @@ class CreateReleaseService < BaseService
end
def success(release)
- out = super()
- out[:release] = release
- out
+ super().merge(release: release)
end
end
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 9884cb96661..95cc9baf406 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,10 +1,10 @@
class CreateSnippetService < BaseService
def execute
- if project.nil?
- snippet = PersonalSnippet.new(params)
- else
- snippet = project.snippets.build(params)
- end
+ snippet = if project
+ project.snippets.build(params)
+ else
+ PersonalSnippet.new(params)
+ end
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(snippet)
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 91ed0e354d0..c0e7ecf6a96 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -9,12 +9,13 @@ class CreateTagService < BaseService
message.strip! if message
new_tag = nil
+
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError
return error("Tag #{tag_name} already exists")
- rescue GitHooksService::PreReceiveError
- return error('Tag creation was rejected by Git hook')
+ rescue GitHooksService::PreReceiveError => ex
+ return error(ex.message)
end
if new_tag
@@ -22,6 +23,7 @@ class CreateTagService < BaseService
CreateReleaseService.new(@project, @current_user).
execute(tag_name, release_description)
end
+
success.merge(tag: new_tag)
else
error("Target #{target} is invalid")
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index fae069ee4a5..332c55581a1 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -5,7 +5,6 @@ class DeleteBranchService < BaseService
repository = project.repository
branch = repository.find_branch(branch_name)
- # No such branch
unless branch
return error('No such branch', 404)
end
@@ -14,36 +13,29 @@ class DeleteBranchService < BaseService
return error('Cannot remove HEAD branch', 405)
end
- # Dont allow remove of protected branch
if project.protected_branch?(branch_name)
return error('Protected branch cant be removed', 405)
end
- # Dont allow user to remove branch if he is not allowed to push
unless current_user.can?(:push_code, project)
return error('You dont have push access to repo', 405)
end
if repository.rm_branch(current_user, branch_name)
- # GitPushService handles execution of services and hooks for branch pushes
success('Branch was removed')
else
error('Failed to remove branch')
end
- rescue GitHooksService::PreReceiveError
- error('Branch deletion was rejected by Git hook')
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
end
def error(message, return_code = 400)
- out = super(message)
- out[:return_code] = return_code
- out
+ super(message).merge(return_code: return_code)
end
def success(message)
- out = super()
- out[:message] = message
- out
+ super().merge(message: message)
end
def build_push_data(branch)
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index de3352a6756..1e41fbe34b6 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -5,7 +5,6 @@ class DeleteTagService < BaseService
repository = project.repository
tag = repository.find_tag(tag_name)
- # No such tag
unless tag
return error('No such tag', 404)
end
@@ -26,15 +25,11 @@ class DeleteTagService < BaseService
end
def error(message, return_code = 400)
- out = super(message)
- out[:return_code] = return_code
- out
+ super(message).merge(return_code: return_code)
end
def success(message)
- out = super()
- out[:message] = message
- out
+ super().merge(message: message)
end
def build_push_data(tag)
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 0326a8823e9..37c5e321b39 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -15,7 +15,6 @@ module Files
params[:file_content]
end
- # Validate parameters
validate
# Create new branch if it different from source_branch
@@ -26,7 +25,7 @@ module Files
if commit
success
else
- error("Something went wrong. Your changes were not committed")
+ error('Something went wrong. Your changes were not committed')
end
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
@@ -43,7 +42,7 @@ module Files
end
def validate
- allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+ allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
unless allowed
raise_error("You are not allowed to push into this branch")
@@ -51,12 +50,12 @@ module Files
unless project.empty_repo?
unless @source_project.repository.branch_names.include?(@source_branch)
- raise_error("You can only create or edit files when you are on a branch")
+ raise_error('You can only create or edit files when you are on a branch')
end
if different_branch?
if repository.branch_names.include?(@target_branch)
- raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes")
+ raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
end
end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index e4cde4a2fd8..8eaf6db8012 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -29,7 +29,7 @@ module Files
blob = repository.blob_at_branch(@source_branch, @file_path)
if blob
- raise_error("Your changes could not be committed because a file with the same name already exists")
+ raise_error('Your changes could not be committed because a file with the same name already exists')
end
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 8f5c3393dfc..172bd85dade 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -3,14 +3,16 @@ class GitHooksService
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
- @user = Gitlab::ShellEnv.gl_id(user)
+ @user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev
@newrev = newrev
@ref = ref
%w(pre-receive update).each do |hook_name|
- unless run_hook(hook_name)
- raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook")
+ status, message = run_hook(hook_name)
+
+ unless status
+ raise PreReceiveError, message
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 66136b62617..a886f35981f 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -53,10 +53,6 @@ class GitPushService < BaseService
# could cause the last commit of a merge request to change.
update_merge_requests
- # Checks if the main language has changed in the project and if so
- # it updates it accordingly
- update_main_language
-
perform_housekeeping
end
@@ -64,19 +60,6 @@ class GitPushService < BaseService
@project.repository.copy_gitattributes(params[:ref])
end
- def update_main_language
- # Performance can be bad so for now only check main_language once
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937
- return if @project.main_language.present?
-
- return unless is_default_branch?
- return unless push_to_new_branch? || push_to_existing_branch?
-
- current_language = @project.repository.main_language
- @project.update_attributes(main_language: current_language)
- true
- end
-
protected
def update_merge_requests
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 7410442609d..58573078048 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -23,9 +23,10 @@ class GitTagPushService < BaseService
commits = []
message = nil
- if !Gitlab::Git.blank_ref?(params[:newrev])
+ unless Gitlab::Git.blank_ref?(params[:newrev])
tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
+
if tag && tag.target == params[:newrev]
commit = project.commit(tag.target)
commits = [commit].compact
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2b16089df1b..e3dc569152c 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project)
params.delete(:milestone_id)
+ params.delete(:add_label_ids)
+ params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_id)
end
@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService
end
def filter_labels
- return if params[:label_ids].to_a.empty?
+ if params[:add_label_ids].present? || params[:remove_label_ids].present?
+ params.delete(:label_ids)
+
+ filter_labels_in_param(:add_label_ids)
+ filter_labels_in_param(:remove_label_ids)
+ else
+ filter_labels_in_param(:label_ids)
+ end
+ end
+
+ def filter_labels_in_param(key)
+ return if params[key].to_a.empty?
- params[:label_ids] =
- project.labels.where(id: params[:label_ids]).pluck(:id)
+ params[key] = project.labels.where(id: params[key]).pluck(:id)
+ end
+
+ def update_issuable(issuable, attributes)
+ issuable.with_transaction_returning_status do
+ add_label_ids = attributes.delete(:add_label_ids)
+ remove_label_ids = attributes.delete(:remove_label_ids)
+
+ issuable.label_ids |= add_label_ids if add_label_ids
+ issuable.label_ids -= remove_label_ids if remove_label_ids
+
+ issuable.assign_attributes(attributes.merge(updated_by: current_user))
+
+ issuable.save
+ end
end
def update(issuable)
@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService
filter_params
old_labels = issuable.labels.to_a
- if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
+ if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable, old_labels: old_labels)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 772f5c5fffa..089b0f527e2 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,6 +1,5 @@
module Issues
class BaseService < ::IssuableBaseService
-
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
index de8387c4900..cd08c3a0cb9 100644
--- a/app/services/issues/bulk_update_service.rb
+++ b/app/services/issues/bulk_update_service.rb
@@ -4,11 +4,12 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",")
issue_params = params
- issue_params.delete(:state_event) unless issue_params[:state_event].present?
- issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
- issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
+ %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key|
+ issue_params.delete(key) unless issue_params[key].present?
+ end
issues = Issue.where(id: issues_ids)
+
issues.each do |issue|
next unless can?(current_user, :update_issue, issue)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index e61628086f0..ab667456db7 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -24,6 +24,7 @@ module Issues
@new_issue = create_new_issue
rewrite_notes
+ rewrite_award_emoji
add_note_moved_from
# Old issue tasks
@@ -72,6 +73,14 @@ module Issues
end
end
+ def rewrite_award_emoji
+ @old_issue.award_emoji.each do |award|
+ new_award = award.dup
+ new_award.awardable = @new_issue
+ new_award.save
+ end
+ end
+
def rewrite_content(content)
return unless content
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 3563cbaa997..c7d406cc331 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -24,6 +24,10 @@ module Issues
todo_service.reassigned_issue(issue, current_user)
end
+ if issue.previous_changes.include?('confidential')
+ create_confidentiality_note(issue)
+ end
+
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
@@ -37,5 +41,11 @@ module Issues
def close_service
Issues::CloseService
end
+
+ private
+
+ def create_confidentiality_note(issue)
+ SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
+ end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
new file mode 100644
index 00000000000..15358f80208
--- /dev/null
+++ b/app/services/members/destroy_service.rb
@@ -0,0 +1,21 @@
+module Members
+ class DestroyService < BaseService
+ attr_accessor :member, :current_user
+
+ def initialize(member, user)
+ @member, @current_user = member, user
+ end
+
+ def execute
+ unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ member.destroy
+
+ if member.request? && member.user != current_user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
new file mode 100644
index 00000000000..566049525cb
--- /dev/null
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -0,0 +1,17 @@
+module MergeRequests
+ class AddTodoWhenBuildFailsService < MergeRequests::BaseService
+ # Adds a todo to the parent merge_request when a CI build fails
+ def execute(commit_status)
+ each_merge_request(commit_status) do |merge_request|
+ todo_service.merge_request_build_failed(merge_request)
+ end
+ end
+
+ # Closes any pending build failed todos for the parent MRs when a build is retried
+ def close(commit_status)
+ each_merge_request(commit_status) do |merge_request|
+ todo_service.merge_request_build_retried(merge_request)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index e6837a18696..bc3606a14c2 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -1,6 +1,5 @@
module MergeRequests
class BaseService < ::IssuableBaseService
-
def create_note(merge_request)
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
@@ -38,5 +37,30 @@ module MergeRequests
def filter_params
super(:merge_request)
end
+
+ 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
+
+ def each_merge_request(commit_status)
+ merge_request_from(commit_status).each do |merge_request|
+ pipeline = merge_request.pipeline
+
+ next unless pipeline
+ next unless pipeline.sha == commit_status.sha
+
+ yield merge_request, pipeline
+ end
+ end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index cd4230aa5e4..7fe57747265 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -66,7 +66,7 @@ module MergeRequests
commits = merge_request.compare_commits
if commits && commits.count == 1
commit = commits.first
- merge_request.title = commit.title
+ merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?)
case issue
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description << closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 33609d01f20..96a25330af1 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -8,11 +8,14 @@ module MergeRequests
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
filter_params
- label_params = params[:label_ids]
- merge_request = MergeRequest.new(params.except(:label_ids))
+ label_params = params.delete(:label_ids)
+ force_remove_source_branch = params.delete(:force_remove_source_branch)
+
+ merge_request = MergeRequest.new(params)
merge_request.source_project = source_project
merge_request.target_project ||= source_project
merge_request.author = current_user
+ merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
if merge_request.save
merge_request.update_attributes(label_ids: label_params)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 9a58383b398..f1b1d90c457 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -34,21 +34,28 @@ module MergeRequests
committer: committer
}
- commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options)
merge_request.update(merge_commit_sha: commit_id)
+ rescue GitHooksService::PreReceiveError => e
+ merge_request.update(merge_error: e.message)
+ false
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
- return false
+ false
end
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch].present?
- DeleteBranchService.new(@merge_request.source_project, current_user).
+ if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
+ DeleteBranchService.new(@merge_request.source_project, branch_deletion_user).
execute(merge_request.source_branch)
end
end
+
+ def branch_deletion_user
+ @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
+ end
end
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 d6af12f9739..4ad5fb08311 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -12,7 +12,7 @@ module MergeRequests
merge_request.merge_when_build_succeeds = true
merge_request.merge_user = @current_user
- SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit)
+ SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
end
merge_request.save
@@ -20,16 +20,10 @@ module MergeRequests
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status)
- merge_requests = merge_request_from(commit_status)
-
- merge_requests.each do |merge_request|
+ each_merge_request(commit_status) do |merge_request, pipeline|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
-
- ci_commit = merge_request.ci_commit
- next unless ci_commit
- next unless ci_commit.sha == commit_status.sha
- next unless ci_commit.success?
+ next unless pipeline.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
@@ -46,21 +40,5 @@ module MergeRequests
error("Can't cancel the automatic merge", 406)
end
end
-
- private
-
- 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
end
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 064910f81f7..8437d9b8b43 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -20,6 +20,7 @@ module MergeRequests
return unless merge_request.target_branch == project.default_branch
closed_issues = merge_request.closes_issues(current_user)
+
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 8b3d56c2b4c..21490ac77ea 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -12,6 +12,7 @@ module MergeRequests
close_merge_requests
reload_merge_requests
reset_merge_when_build_succeeds
+ mark_pending_todos_done
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -33,10 +34,10 @@ module MergeRequests
def close_merge_requests
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a
- merge_requests = merge_requests.select(&:last_commit)
+ merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
- commit_ids.include?(merge_request.last_commit.id)
+ commit_ids.include?(merge_request.diff_head_sha)
end
merge_requests.uniq.select(&:source_project).each do |merge_request|
@@ -59,7 +60,7 @@ module MergeRequests
merge_requests.each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
- merge_request.reload_code
+ merge_request.reload_diff
merge_request.mark_as_unchecked
else
mr_commit_ids = merge_request.commits.map(&:id)
@@ -67,7 +68,7 @@ module MergeRequests
matches = mr_commit_ids & push_commit_ids
if matches.any?
- merge_request.reload_code
+ merge_request.reload_diff
merge_request.mark_as_unchecked
else
merge_request.mark_as_unchecked
@@ -80,6 +81,12 @@ module MergeRequests
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end
+ def mark_pending_todos_done
+ merge_requests_for_source_branch.each do |merge_request|
+ todo_service.merge_request_push(merge_request, @current_user)
+ end
+ end
+
def find_new_commits
if branch_added?
@commits = []
@@ -87,12 +94,10 @@ module MergeRequests
merge_request = merge_requests_for_source_branch.first
return unless merge_request
- last_commit = merge_request.last_commit
-
begin
# Since any number of commits could have been made to the restored branch,
# find the common root to see what has been added.
- common_ref = @project.repository.merge_base(last_commit.id, @newrev)
+ common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev)
# If the a commit no longer exists in this repo, gitlab_git throws
# a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
@commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 8279ad2001b..eb88ae9d11c 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
create_note(merge_request)
notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
- merge_request.reload_code
+ merge_request.reload_diff
merge_request.mark_as_unchecked
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 477c64e7377..026a37997d4 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,6 +11,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+
update(merge_request)
end
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 2414966505b..e457212508f 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -1,7 +1,6 @@
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
-
Milestone.transaction do
update_params = { milestone: nil }
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 01586994813..02fca5c0ea3 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -5,7 +5,12 @@ module Notes
note.author = current_user
note.system = false
- return unless valid_project?(note)
+ if note.award_emoji?
+ noteable = note.noteable
+ todo_service.new_award_emoji(noteable, current_user)
+
+ return noteable.create_award_emoji(note.award_emoji_name, current_user)
+ end
if note.save
# Finish the harder work in the background
@@ -15,14 +20,5 @@ module Notes
note
end
-
- private
-
- def valid_project?(note)
- return false unless project
- return true if note.for_commit?
-
- note.noteable.try(:project) == project
- end
end
end
diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb
new file mode 100644
index 00000000000..0cb731f5bc3
--- /dev/null
+++ b/app/services/notes/diff_position_update_service.rb
@@ -0,0 +1,30 @@
+module Notes
+ class DiffPositionUpdateService < BaseService
+ def execute(note)
+ new_position = tracer.trace(note.position)
+
+ # Don't update the position if the type doesn't match, since that means
+ # the diff line commented on was changed, and the comment is now outdated
+ old_position = note.position
+ if new_position &&
+ new_position != old_position &&
+ new_position.type == old_position.type
+
+ note.position = new_position
+ end
+
+ note
+ end
+
+ private
+
+ def tracer
+ @tracer ||= Gitlab::Diff::PositionTracer.new(
+ repository: project.repository,
+ old_diff_refs: params[:old_diff_refs],
+ new_diff_refs: params[:new_diff_refs],
+ paths: params[:paths]
+ )
+ end
+ end
+end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index e818f58d13c..534c48aefff 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -8,7 +8,7 @@ module Notes
def execute
# Skip system notes, like status changes and cross-references and awards
- unless @note.system || @note.is_award
+ unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
@note.create_cross_references!
execute_note_hooks
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 42ec1ac9e1a..ab6e51209ee 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -29,9 +29,10 @@ class NotificationService
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the issue's labels
+ # * users with custom level checked with "new issue"
#
def new_issue(issue, current_user)
- new_resource_email(issue, issue.project, 'new_issue_email')
+ new_resource_email(issue, issue.project, :new_issue_email)
end
# When we close an issue we should send an email to:
@@ -39,18 +40,20 @@ class NotificationService
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * users with custom level checked with "close issue"
#
def close_issue(issue, current_user)
- close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
+ close_resource_email(issue, issue.project, current_user, :closed_issue_email)
end
# 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
+ # * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user)
- reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
+ 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:
@@ -58,7 +61,7 @@ class NotificationService
# * 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')
+ relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -66,18 +69,20 @@ class NotificationService
# * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
+ # * users with custom level checked with "new merge request"
#
def new_merge_request(merge_request, current_user)
- new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
+ new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
end
# 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
+ # * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
+ 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:
@@ -85,15 +90,15 @@ class NotificationService
# * 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')
+ 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')
+ close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email)
end
def reopen_issue(issue, current_user)
- reopen_resource_email(issue, issue.project, current_user, 'issue_status_changed_email', 'reopened')
+ reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened')
end
def merge_mr(merge_request, current_user)
@@ -101,7 +106,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merged_merge_request_email'
+ :merged_merge_request_email
)
end
@@ -110,7 +115,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merge_request_status_email',
+ :merge_request_status_email,
'reopened'
)
end
@@ -130,8 +135,7 @@ class NotificationService
# ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed')
- return true if note.cross_reference? && note.system == true
- return true if note.is_award
+ return true if note.cross_reference? && note.system?
target = note.noteable
@@ -149,11 +153,15 @@ class NotificationService
else
mentioned_users
end
+
recipients = recipients.concat(participants)
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
recipients = recipients + mentioned_users
@@ -169,21 +177,33 @@ class NotificationService
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
+
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
end
+ # Members
+ def new_access_request(member)
+ mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
+ end
+
+ def decline_access_request(member)
+ mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
+ end
+
+ # Project invite
def invite_project_member(project_member, token)
- mailer.project_member_invited_email(project_member.id, token).deliver_later
+ mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
- mailer.project_invite_accepted_email(project_member.id).deliver_later
+ mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end
def decline_project_invite(project_member)
- mailer.project_invite_declined_email(
+ mailer.member_invite_declined_email(
+ project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
project_member.access_level,
@@ -192,23 +212,25 @@ class NotificationService
end
def new_project_member(project_member)
- mailer.project_access_granted_email(project_member.id).deliver_later
+ mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
def update_project_member(project_member)
- mailer.project_access_granted_email(project_member.id).deliver_later
+ mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
+ # Group invite
def invite_group_member(group_member, token)
- mailer.group_member_invited_email(group_member.id, token).deliver_later
+ mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
end
def accept_group_invite(group_member)
- mailer.group_invite_accepted_email(group_member.id).deliver_later
+ mailer.member_invite_accepted_email(group_member.real_source_type, group_member.id).deliver_later
end
def decline_group_invite(group_member)
- mailer.group_invite_declined_email(
+ mailer.member_invite_declined_email(
+ group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
group_member.access_level,
@@ -217,11 +239,11 @@ class NotificationService
end
def new_group_member(group_member)
- mailer.group_access_granted_email(group_member.id).deliver_later
+ mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def update_group_member(group_member)
- mailer.group_access_granted_email(group_member.id).deliver_later
+ mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
@@ -247,14 +269,41 @@ class NotificationService
end
end
+ def project_exported(project, current_user)
+ mailer.project_was_exported_email(current_user, project).deliver_later
+ end
+
+ def project_not_exported(project, current_user, errors)
+ mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
+ end
+
protected
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, project, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += notification_settings_for(project, :custom, action)
+ user_ids += notification_settings_for(project.group, :custom, action)
+
+ # Users with global level custom
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
+
+ global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
+ user_ids += users_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
# Get project users with WATCH notification level
def project_watchers(project)
- project_members = project_member_notification(project)
+ project_members = notification_settings_for(project)
+
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
- users_with_project_level_global = project_member_notification(project, :global)
- users_with_group_level_global = group_member_notification(project, :global)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
@@ -263,32 +312,39 @@ class NotificationService
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
- def project_member_notification(project, notification_level=nil)
+ def notification_settings_for(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
if notification_level
- project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
else
- project.notification_settings.pluck(:user_id)
+ resource.notification_settings.pluck(:user_id)
end
end
- def group_member_notification(project, notification_level)
- if project.group
- project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
- else
- []
- end
+ def users_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
end
- def users_with_global_level_watch(ids)
- User.where(
- id: ids,
- notification_level: NotificationSetting.levels[:watch]
- ).pluck(:id)
+ def users_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
end
# Build a list of users based on project notifcation settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = project_member_notification(project, :watch)
+ users = notification_settings_for(project, :watch)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
@@ -302,7 +358,7 @@ class NotificationService
# Build a list of users based on group notification settings
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = group_member_notification(project, :watch)
+ uids = notification_settings_for(project, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -323,7 +379,7 @@ class NotificationService
end
def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact.uniq
+ recipients.concat(project_watchers(project)).compact
end
# Remove users with disabled notifications from array
@@ -353,7 +409,9 @@ class NotificationService
users = users.reject(&:blocked?)
users.reject do |user|
- next user.notification_level == level unless project
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
setting = user.notification_settings_for(project)
@@ -362,13 +420,13 @@ class NotificationService
end
# reject users who globally set mention notification and has no setting per project/group
- next user.notification_level == level unless setting
+ next global_notification_setting.level == level unless setting
# reject users who set mention notification in project
next true if setting.level == level
# reject users who have mention level in project and disabled in global settings
- setting.global? && user.notification_level == level
+ setting.global? && global_notification_setting.level == level
end
end
@@ -406,7 +464,7 @@ class NotificationService
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: :new)
+ recipients = build_recipients(target, project, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -414,7 +472,8 @@ class NotificationService
end
def close_resource_email(target, project, current_user, method)
- recipients = build_recipients(target, project, current_user)
+ action = method == :merged_merge_request_email ? "merge" : "close"
+ recipients = build_recipients(target, project, current_user, action: action)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -425,7 +484,7 @@ class NotificationService
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, action: :reassign, previous_assignee: previous_assignee)
+ recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
recipients.each do |recipient|
mailer.send(
@@ -448,7 +507,7 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user)
+ recipients = build_recipients(target, project, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
@@ -456,15 +515,20 @@ class NotificationService
end
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
- recipients = target.participants(current_user)
+ custom_action = build_custom_key(action, target)
+ recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
+
+ recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
+ recipients = recipients.uniq
+
# 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
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
recipients << previous_assignee if previous_assignee
recipients << target.assignee
end
@@ -472,7 +536,7 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
- if action == :new
+ if [:new_issue, :new_merge_request].include?(custom_action)
recipients = add_labels_subscribers(recipients, target)
end
@@ -502,4 +566,10 @@ class NotificationService
end
end
end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.name.underscore}".to_sym
+ end
end
diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb
index 6194f6ce91e..264fdccde8f 100644
--- a/app/services/oauth2/access_token_validation_service.rb
+++ b/app/services/oauth2/access_token_validation_service.rb
@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService
end
protected
+
# True if the token's scope is a superset of required scopes,
# or the required scopes is empty.
def sufficient_scope?(token, scopes)
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index ba50305dbd5..23b6668e0d1 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -4,8 +4,16 @@ module Projects
@project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
+ def milestones
+ @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title])
+ end
+
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
+
+ def labels
+ @project.labels.select([:title, :color])
+ end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 501e58c1407..55956be2844 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -6,6 +6,7 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
+ import_data = params.delete(:import_data)
@project = Project.new(params)
@@ -49,22 +50,20 @@ module Projects
@project.build_forked_project_link(forked_from_project_id: forked_from_project_id)
end
- Project.transaction do
- @project.save
+ save_project_and_import_data(import_data)
- if @project.persisted? && !@project.import?
- raise 'Failed to create repository' unless @project.create_repository
- end
- end
+ @project.import_start if @project.import?
after_create_actions if @project.persisted?
+ if @project.errors.empty?
+ @project.add_import_job if @project.import?
+ else
+ fail(error: @project.errors.full_messages.join(', '))
+ end
@project
rescue => e
- message = "Unable to save project: #{e.message}"
- Rails.logger.error(message)
- @project.errors.add(:base, message) if @project
- @project
+ fail(error: e.message)
end
protected
@@ -81,20 +80,44 @@ module Projects
def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
- @project.create_wiki if @project.wiki_enabled?
+ unless @project.gitlab_project_import?
+ @project.create_wiki if @project.wiki_enabled?
- @project.build_missing_services
+ @project.build_missing_services
- @project.create_labels
+ @project.create_labels
+ end
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
- unless @project.group
+ unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user]
end
+ end
- @project.import_start if @project.import?
+ def save_project_and_import_data(import_data)
+ Project.transaction do
+ @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
+
+ if @project.save && !@project.import?
+ raise 'Failed to create repository' unless @project.create_repository
+ end
+ end
+ end
+
+ def fail(error:)
+ message = "Unable to save project. Error: #{error}"
+ message << "Project ID: #{@project.id}" if @project && @project.id
+
+ Rails.logger.error(message)
+
+ if @project && @project.import?
+ @project.errors.add(:base, message)
+ @project.mark_import_as_failed(message)
+ end
+
+ @project
end
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 19aab999e00..882606e38d0 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -26,6 +26,10 @@ module Projects
Project.transaction do
project.destroy!
+ unless remove_registry_tags
+ raise_error('Failed to remove project container registry. Please try again or contact administrator')
+ end
+
unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator')
end
@@ -35,7 +39,7 @@ module Projects
end
end
- log_info("Project \"#{project.name}\" was removed")
+ log_info("Project \"#{project.path_with_namespace}\" was removed")
system_hook_service.execute_hooks_for(project, :destroy)
true
end
@@ -47,18 +51,24 @@ module Projects
return true if params[:skip_repo] == true
# There is a possibility project does not have repository or wiki
- return true unless gitlab_shell.exists?(path + '.git')
+ return true unless gitlab_shell.exists?(project.repository_storage_path, path + '.git')
new_path = removal_path(path)
- if gitlab_shell.mv_repository(path, new_path)
+ if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path)
log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
- GitlabShellWorker.perform_in(5.minutes, :remove_repository, new_path)
+ GitlabShellWorker.perform_in(5.minutes, :remove_repository, project.repository_storage_path, new_path)
else
false
end
end
+ def remove_registry_tags
+ return true unless Gitlab.config.registry.enabled
+
+ project.container_registry_repository.delete_tags
+ end
+
def raise_error(message)
raise DestroyError.new(message)
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index 6386f57fb0d..f06a3d44c17 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -1,6 +1,5 @@
module Projects
class DownloadService < BaseService
-
WHITELIST = [
/^[^.]+\.fogbugz.com$/
]
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 0577ae778d5..de6dc38cc8e 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -3,7 +3,7 @@ module Projects
def execute
new_params = {
forked_from_project_id: @project.id,
- visibility_level: @project.visibility_level,
+ visibility_level: allowed_visibility_level,
description: @project.description,
name: @project.name,
path: @project.path,
@@ -19,5 +19,17 @@ module Projects
new_project = CreateService.new(current_user, new_params).execute
new_project
end
+
+ private
+
+ def allowed_visibility_level
+ project_level = @project.visibility_level
+
+ if Gitlab::VisibilityLevel.non_restricted_level?(project_level)
+ project_level
+ else
+ Gitlab::VisibilityLevel.highest_allowed_level
+ end
+ end
end
end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 3b7c36f0908..752c11d7ae6 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -22,12 +22,12 @@ module Projects
end
def execute
- raise LeaseTaken if !try_obtain_lease
+ raise LeaseTaken unless try_obtain_lease
- GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
+ GitlabShellOneShotWorker.perform_async(:gc, @project.repository_storage_path, @project.path_with_namespace)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
- @project.update_column(:pushes_since_gc, 0)
+ update_pushes_since_gc(0)
end
end
@@ -37,12 +37,18 @@ module Projects
def increment!
Gitlab::Metrics.measure(:increment_pushes_since_gc) do
- @project.increment!(:pushes_since_gc)
+ update_pushes_since_gc(@project.pushes_since_gc + 1)
end
end
private
+ def update_pushes_since_gc(new_value)
+ if Gitlab::ExclusiveLease.new("project_housekeeping:update_pushes_since_gc:#{project.id}", timeout: 60).try_obtain
+ @project.update_column(:pushes_since_gc, new_value)
+ end
+ end
+
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
new file mode 100644
index 00000000000..6afc048576d
--- /dev/null
+++ b/app/services/projects/import_export/export_service.rb
@@ -0,0 +1,60 @@
+module Projects
+ module ImportExport
+ class ExportService < BaseService
+ def execute(_options = {})
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work'))
+ save_all
+ end
+
+ private
+
+ def save_all
+ if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ Gitlab::ImportExport::Saver.save(shared: @shared)
+ notify_success
+ else
+ cleanup_and_notify
+ end
+ end
+
+ def version_saver
+ Gitlab::ImportExport::VersionSaver.new(shared: @shared)
+ end
+
+ def project_tree_saver
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ end
+
+ def uploads_saver
+ Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared)
+ end
+
+ def repo_saver
+ Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared)
+ end
+
+ def wiki_repo_saver
+ Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
+ end
+
+ def cleanup_and_notify
+ Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
+
+ FileUtils.rm_rf(@shared.export_path)
+
+ notify_error
+ raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
+ end
+
+ def notify_success
+ Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
+
+ notification_service.project_exported(@project, @current_user)
+ end
+
+ def notify_error
+ notification_service.project_not_exported(@project, @current_user, @shared.errors)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index ef15ef6a473..163ebf26c84 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -9,26 +9,31 @@ module Projects
'fogbugz',
'gitlab',
'github',
- 'google_code'
+ 'google_code',
+ 'gitlab_project'
]
def execute
- if unknown_url?
- # In this case, we only want to import issues, not a repository.
- create_repository
- else
- import_repository
- end
+ add_repository_to_project unless project.gitlab_project_import?
import_data
success
- rescue Error => e
+ rescue => e
error(e.message)
end
private
+ def add_repository_to_project
+ if unknown_url?
+ # In this case, we only want to import issues, not a repository.
+ create_repository
+ else
+ import_repository
+ end
+ end
+
def create_repository
unless project.create_repository
raise Error, 'The repository could not be created.'
@@ -37,16 +42,16 @@ module Projects
def import_repository
begin
- gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
+ gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue Gitlab::Shell::Error => e
- raise Error, e.message
+ raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
def import_data
return unless has_importer?
- project.repository.before_import
+ project.repository.before_import unless project.gitlab_project_import?
unless importer.execute
raise Error, 'The remote data could not be imported.'
@@ -58,6 +63,8 @@ module Projects
end
def importer
+ return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
+
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project)
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 111b3ec05ea..bc7f8bf433b 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -34,6 +34,11 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists")
end
+ if project.has_container_registry_tags?
+ # we currently doesn't support renaming repository if it contains tags in container registry
+ raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
+ end
+
project.expire_caches_before_rename(old_path)
# Apply new namespace id and visibility level
@@ -45,12 +50,12 @@ module Projects
project.send_move_instructions(old_path)
# Move main repository
- unless gitlab_shell.mv_repository(old_path, new_path)
+ unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path)
raise TransferError.new('Cannot move project')
end
# Move wiki repo also if present
- gitlab_shell.mv_repository("#{old_path}.wiki", "#{new_path}.wiki")
+ gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
# clear project cached events
project.reset_events_cache
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 941df08995c..f06311511cc 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,10 +3,11 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
+
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
-
+
deny_visibility_level(project, new_visibility)
return project
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index e43b5b51e5b..1fb72cf89e9 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -85,7 +85,7 @@ class SystemHooksService
path_with_namespace: model.path_with_namespace,
project_id: model.id,
owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
+ owner_email: owner.respond_to?(:email) ? owner.email : "",
project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
}
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 4bdb1b0c074..1ab3b5789bc 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -82,7 +82,7 @@ class SystemNoteService
end
body << ' ' << 'label'.pluralize(labels_count)
- body = "#{body.capitalize}"
+ body = body.capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -125,7 +125,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
- body += " by #{source.gfm_reference(project)}" if source
+ body << " by #{source.gfm_reference(project)}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -139,7 +139,7 @@ class SystemNoteService
# Called when 'merge when build succeeds' is canceled
def self.cancel_merge_when_build_succeeds(noteable, project, author)
- body = "Canceled the automatic merge"
+ body = 'Canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -169,12 +169,33 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_title(noteable, project, author, old_title)
- return unless noteable.respond_to?(:title)
+ new_title = noteable.title.dup
- body = "Title changed from **#{old_title}** to **#{noteable.title}**"
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
+
+ marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
+ marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
+
+ body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when the confidentiality changes
+ #
+ # issue - Issue object
+ # project - Project owning the issue
+ # author - User performing the change
+ #
+ # Example Note text:
+ #
+ # "Made the issue confidential"
+ #
+ # Returns the created Note object
+ def self.change_issue_confidentiality(issue, project, author)
+ body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a branch in Noteable is changed
#
# noteable - Noteable object
@@ -215,6 +236,7 @@ class SystemNoteService
else
'deleted'
end
+
body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -272,7 +294,6 @@ class SystemNoteService
end
end
-
def self.cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 42c5bca90fd..6bb0a72d30e 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -1,6 +1,6 @@
# TodoService class
#
-# Used for creating todos after certain user actions
+# Used for creating/updating todos after certain user actions
#
# Ex.
# TodoService.new.new_issue(issue, current_user)
@@ -20,7 +20,7 @@ class TodoService
# * 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)
+ update_issuable(issue, current_user)
end
# When close an issue we should:
@@ -53,7 +53,7 @@ class TodoService
# * 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)
+ update_issuable(merge_request, current_user)
end
# When close a merge request we should:
@@ -80,6 +80,30 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
+ # When a build fails on the HEAD of a merge request we should:
+ #
+ # * create a todo for that user to fix it
+ #
+ def merge_request_build_failed(merge_request)
+ create_build_failed_todo(merge_request)
+ end
+
+ # When a new commit is pushed to a merge request we should:
+ #
+ # * mark all pending todos related to the merge request for that user as done
+ #
+ def merge_request_push(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When a build is retried to a merge request we should:
+ #
+ # * mark all pending todos related to the merge request for the author as done
+ #
+ def merge_request_build_retried(merge_request)
+ mark_pending_todos_as_done(merge_request, merge_request.author)
+ end
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -98,6 +122,14 @@ class TodoService
handle_note(note, current_user)
end
+ # When an emoji is awarded we should:
+ #
+ # * mark all pending todos related to the awardable for the current user as done
+ #
+ def new_award_emoji(awardable, current_user)
+ mark_pending_todos_as_done(awardable, 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
@@ -105,14 +137,31 @@ class TodoService
def mark_pending_todos_as_done(target, user)
attributes = attributes_for_target(target)
pending_todos(user, attributes).update_all(state: :done)
+ user.update_todos_count_cache
+ end
+
+ # When user marks some todos as done
+ def mark_todos_as_done(todos, current_user)
+ todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
+
+ todos.update_all(state: :done)
+ current_user.update_todos_count_cache
+ end
+
+ # When user marks an issue as todo
+ def mark_todo(issuable, current_user)
+ attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
+ create_todos(current_user, attributes)
end
private
def create_todos(users, attributes)
- Array(users).each do |user|
+ Array(users).map do |user|
next if pending_todos(user, attributes).exists?
- Todo.create(attributes.merge(user_id: user.id))
+ todo = Todo.create(attributes.merge(user_id: user.id))
+ user.update_todos_count_cache
+ todo
end
end
@@ -121,6 +170,18 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
+ def update_issuable(issuable, author)
+ # Skip toggling a task list item in a description
+ return if toggling_tasks?(issuable)
+
+ create_mention_todos(issuable.project, issuable, author)
+ end
+
+ def toggling_tasks?(issuable)
+ issuable.previous_changes.include?('description') &&
+ issuable.tasks? && issuable.updated_tasks.any?
+ end
+
def handle_note(note, author)
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
@@ -145,6 +206,12 @@ class TodoService
create_todos(mentioned_users, attributes)
end
+ def create_build_failed_todo(merge_request)
+ author = merge_request.author
+ attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
+ create_todos(author, attributes)
+ end
+
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
@@ -170,7 +237,7 @@ class TodoService
end
def filter_mentioned_users(project, target, author)
- mentioned_users = target.mentioned_users
+ mentioned_users = target.mentioned_users(author)
mentioned_users = reject_users_without_access(mentioned_users, project, target)
mentioned_users.delete(author)
mentioned_users.uniq
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index 25eb13ef09a..0ee1ff2d7d9 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -2,7 +2,6 @@ require_relative 'base_service'
class UpdateReleaseService < BaseService
def execute(tag_name, release_description)
-
repository = project.repository
existing_tag = repository.find_tag(tag_name)
@@ -22,8 +21,6 @@ class UpdateReleaseService < BaseService
end
def success(release)
- out = super()
- out[:release] = release
- out
+ super().merge(release: release)
end
end
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index 93af8f21972..a6bb36821c3 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -9,6 +9,7 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
+
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 9162f128602..14317ea65c8 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -1,14 +1,12 @@
module WikiPages
class BaseService < ::BaseService
-
def hook_data(page, action)
hook_data = {
object_kind: page.class.name.underscore,
user: current_user.hook_attrs,
project: @project.hook_attrs,
- object_attributes: page.hook_attrs,
- # DEPRECATED
- repository: @project.hook_attrs.slice(:name, :url, :description, :homepage)
+ wiki: @project.wiki.hook_attrs,
+ object_attributes: page.hook_attrs
}
page_url = Gitlab::UrlBuilder.build(page)
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 28085b31083..046a1d641a9 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -4,7 +4,7 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
- "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}"
+ "#{Gitlab.config.lfs.storage_path}/#{model.oid[0, 2]}/#{model.oid[2, 2]}"
end
def cache_dir
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
new file mode 100644
index 00000000000..09bfa613cbe
--- /dev/null
+++ b/app/validators/addressable_url_validator.rb
@@ -0,0 +1,45 @@
+# AddressableUrlValidator
+#
+# Custom validator for URLs. This is a stricter version of UrlValidator - it also checks
+# for using the right protocol, but it actually parses the URL checking for any syntax errors.
+# The regex is also different from `URI` as we use `Addressable::URI` here.
+#
+# By default, only URLs for http, https, ssh, and git protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, addressable_url: true
+#
+# validates :ftp_url, addressable_url: { protocols: %w(ftp) }
+#
+# validates :git_url, addressable_url: { protocols: %w(http https ssh git) }
+# end
+#
+class AddressableUrlValidator < ActiveModel::EachValidator
+ DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }
+
+ def validate_each(record, attribute, value)
+ unless valid_url?(value)
+ record.errors.add(attribute, "must be a valid URL")
+ end
+ end
+
+ private
+
+ def valid_url?(value)
+ return false unless value
+
+ valid_protocol?(value) && valid_uri?(value)
+ end
+
+ def valid_uri?(value)
+ Gitlab::UrlSanitizer.valid?(value)
+ end
+
+ def valid_protocol?(value)
+ options = DEFAULT_OPTIONS.merge(self.options)
+ value =~ /\A#{URI.regexp(options[:protocols])}\z/
+ end
+end
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 2ab01704b77..dd2e7ebd030 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -3,20 +3,20 @@
%tr
%td
- if user
- = link_to user.name, [:admin, user]
+ = link_to user.name, user
.light.small
Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- if reporter
- = link_to reporter.name, [:admin, reporter]
+ = link_to reporter.name, reporter
- else
(removed)
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- = markdown(abuse_report.message.squish!, pipeline: :single_line)
+ = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index d88f3ad314d..dc083e50178 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -46,7 +46,7 @@
Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
= link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index dd4a64e80bc..6c51639b840 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,29 +1,9 @@
- page_title "Preview | Appearance"
-%h3.page-title
- Appearance settings - Preview
-%hr
+.login-box
+ .login-heading
+ %h3 Existing user? Sign in
+ %form
+ = 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"
-.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
index 089e8e4cb7a..454b779842c 100644
--- a/app/views/admin/appearances/show.html.haml
+++ b/app/views/admin/appearances/show.html.haml
@@ -1,7 +1,9 @@
- page_title "Appearance"
+
%h3.page-title
Appearance settings
%p.light
You can modify the look and feel of GitLab here
+%hr
= render 'form'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index e0d8d16a954..8de28528cda 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -15,7 +15,7 @@
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.group-visibility-level-holder
+ .form-group.project-visibility-level-holder
= f.label :default_group_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
@@ -44,6 +44,12 @@
and GitLab.com
= link_to "(?)", help_page_path("integration", "gitlab")
.form-group
+ %label.control-label.col-sm-2 Enabled Git access protocols
+ .col-sm-10
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %span.help-block#clone-protocol-help
+ Allow only the selected protocols to be used for Git access.
+ .form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :version_check_enabled do
@@ -94,6 +100,13 @@
= f.label :user_oauth_applications do
= f.check_box :user_oauth_applications
Allow users to register any application to use GitLab as an OAuth provider
+ .form-group
+ = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :user_default_external do
+ = f.check_box :user_default_external
+ Newly registered users will by default be external
%fieldset
%legend Sign-in Restrictions
@@ -106,9 +119,22 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
+ = f.label :send_user_confirmation_email do
+ = f.check_box :send_user_confirmation_email
+ Send confirmation email on sign-up
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
= f.label :signin_enabled do
= f.check_box :signin_enabled
Sign-in enabled
+ - if omniauth_enabled? && button_based_providers.any?
+ .form-group
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2'
+ .col-sm-10
+ .btn-group{ data: { toggle: 'buttons' } }
+ - oauth_providers_checkboxes.each do |source|
+ = source
.form-group
= f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
@@ -142,6 +168,11 @@
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group
+ = f.label :after_sign_up_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+ .form-group
= f.label :help_page_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :help_page_text, class: 'form-control', rows: 4
@@ -165,6 +196,14 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
+ - if Gitlab.config.registry.enabled
+ %fieldset
+ %legend Container Registry
+ .form-group
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+
%fieldset
%legend Metrics
%p
@@ -285,6 +324,15 @@
= f.text_field :sentry_dsn, class: 'form-control'
%fieldset
+ %legend Repository Storage
+ .form-group
+ = f.label :repository_storage, 'Storage path for new projects', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control'
+ .help-block
+ You can manage the repository storage paths in your gitlab.yml configuration file
+
+ %fieldset
%legend Repository Checks
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index e9c7ca9d5aa..ecc46d86afe 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Settings"
+
%h3.page-title Settings
%hr
= render 'form'
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
new file mode 100644
index 00000000000..9d722bd7382
--- /dev/null
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -0,0 +1,18 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index de5bc050cf0..4f680b507c4 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,46 +1,50 @@
+- @no_container = true
- page_title "Background Jobs"
-%h3.page-title Background Jobs
-%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
+= render 'admin/background_jobs/head'
-%hr
+%div{ class: container_class }
+ %h3.page-title Background Jobs
+ %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
-.panel.panel-default
- .panel-heading Sidekiq running processes
- .panel-body
- - if @sidekiq_processes.empty?
- %h4.cred
- %i.fa.fa-exclamation-triangle
- There are no running sidekiq processes. Please restart GitLab
- - else
- .table-holder
- %table.table
- %thead
- %th USER
- %th PID
- %th CPU
- %th MEM
- %th STATE
- %th START
- %th COMMAND
- %tbody
- - @sidekiq_processes.each do |process|
- - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
- - data = process.strip.split(' ')
- %tr
- %td= gitlab_config.user
- - 5.times do
- %td= data.shift
- %td= data.join(' ')
+ %hr
- .clearfix
- %p
- %i.fa.fa-exclamation-circle
- If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
- %p
- %i.fa.fa-exclamation-circle
- If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
+ .panel.panel-default
+ .panel-heading Sidekiq running processes
+ .panel-body
+ - if @sidekiq_processes.empty?
+ %h4.cred
+ %i.fa.fa-exclamation-triangle
+ There are no running sidekiq processes. Please restart GitLab
+ - else
+ .table-holder
+ %table.table
+ %thead
+ %th USER
+ %th PID
+ %th CPU
+ %th MEM
+ %th STATE
+ %th START
+ %th COMMAND
+ %tbody
+ - @sidekiq_processes.each do |process|
+ - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
+ - data = process.strip.split(' ')
+ %tr
+ %td= gitlab_config.user
+ - 5.times do
+ %td= data.shift
+ %td= data.join(' ')
+ .clearfix
+ %p
+ %i.fa.fa-exclamation-circle
+ If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
+ %p
+ %i.fa.fa-exclamation-circle
+ If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
-.panel.panel-default
- %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
+
+ .panel.panel-default
+ %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 804d7851bdb..1e60205f91a 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -1,50 +1,54 @@
-.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= 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= number_with_delimiter(@all_builds.finished.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
-
-.row-content-block.second-block
- #{(@scope || 'running').capitalize} builds
-
-%ul.content-list
- - if @builds.blank?
- %li
- .nothing-here-block No builds to show
- - else
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Project
- %th Commit
- %th Ref
- %th Runner
- %th Name
- %th Tags
- %th Duration
- %th Finished at
- %th
-
- - @builds.each do |build|
- = render "admin/builds/build", build: build
-
- = paginate @builds, theme: 'gitlab'
-
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: container_class }
+
+ .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= 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= number_with_delimiter(@all_builds.finished.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
+
+ .row-content-block.second-block
+ #{(@scope || 'all').capitalize} builds
+
+ %ul.content-list
+ - if @builds.blank?
+ %li
+ .nothing-here-block No builds to show
+ - else
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Project
+ %th Commit
+ %th Ref
+ %th Runner
+ %th Name
+ %th Tags
+ %th Duration
+ %th Finished at
+ %th
+
+ - @builds.each do |build|
+ = render "admin/builds/build", build: build
+
+ = paginate @builds, theme: 'gitlab'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
new file mode 100644
index 00000000000..b74da64f82e
--- /dev/null
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -0,0 +1,26 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 6dd2fef395d..a2ac407c159 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,155 +1,159 @@
-.admin-dashboard.prepend-top-default
- .row
- .col-md-4
- %h4 Statistics
- %hr
- %p
- Forks
- %span.light.pull-right
- = number_with_delimiter(ForkedProjectLink.count)
- %p
- Issues
- %span.light.pull-right
- = number_with_delimiter(Issue.count)
- %p
- Merge Requests
- %span.light.pull-right
- = number_with_delimiter(MergeRequest.count)
- %p
- Notes
- %span.light.pull-right
- = number_with_delimiter(Note.count)
- %p
- Snippets
- %span.light.pull-right
- = number_with_delimiter(Snippet.count)
- %p
- SSH Keys
- %span.light.pull-right
- = number_with_delimiter(Key.count)
- %p
- Milestones
- %span.light.pull-right
- = number_with_delimiter(Milestone.count)
- %p
- Active Users
- %span.light.pull-right
- = number_with_delimiter(User.active.count)
- .col-md-4
- %h4
- Features
- %hr
- %p
- Sign up
- %span.light.pull-right
- = boolean_to_icon signup_enabled?
- %p
- LDAP
- %span.light.pull-right
- = boolean_to_icon Gitlab.config.ldap.enabled
- %p
- Gravatar
- %span.light.pull-right
- = boolean_to_icon gravatar_enabled?
- %p
- OmniAuth
- %span.light.pull-right
- = boolean_to_icon Gitlab.config.omniauth.enabled
- %p
- Reply by email
- %span.light.pull-right
- = boolean_to_icon Gitlab::IncomingEmail.enabled?
- .col-md-4
- %h4
- Components
- - if current_application_settings.version_check_enabled
- .pull-right
- = version_status_badge
+- @no_container = true
+= render "admin/dashboard/head"
- %hr
- %p
- GitLab
- %span.pull-right
- = Gitlab::VERSION
- %p
- GitLab Shell
- %span.pull-right
- = Gitlab::Shell.new.version
- %p
- GitLab API
- %span.pull-right
- = API::API::version
- %p
- Git
- %span.pull-right
- = Gitlab::Git.version
- %p
- Ruby
- %span.pull-right
- #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
-
- %p
- Rails
- %span.pull-right
- #{Rails::VERSION::STRING}
-
- %p
- = Gitlab::Database.adapter_name
- %span.pull-right
- = Gitlab::Database.version
- %hr
- .row
- .col-sm-4
- .light-well
- %h4 Projects
- .data
- = link_to admin_namespaces_projects_path do
- %h1= number_with_delimiter(Project.count)
- %hr
- = link_to('New Project', new_project_path, class: "btn btn-new")
- .col-sm-4
- .light-well
- %h4 Users
- .data
- = link_to admin_users_path do
- %h1= number_with_delimiter(User.count)
- %hr
- = link_to 'New User', new_admin_user_path, class: "btn btn-new"
- .col-sm-4
- .light-well
- %h4 Groups
- .data
- = link_to admin_groups_path do
- %h1= number_with_delimiter(Group.count)
- %hr
- = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
-
- .row.prepend-top-10
- .col-md-4
- %h4 Latest projects
- %hr
- - @projects.each do |project|
+%div{ class: container_class }
+ .admin-dashboard.prepend-top-default
+ .row
+ .col-md-4
+ %h4 Statistics
+ %hr
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ Forks
%span.light.pull-right
- #{time_ago_with_tooltip(project.created_at)}
-
- .col-md-4
- %h4 Latest users
- %hr
- - @users.each do |user|
+ = number_with_delimiter(ForkedProjectLink.count)
%p
- = link_to [:admin, user], class: 'str-truncated' do
- = user.name
+ Issues
%span.light.pull-right
- #{time_ago_with_tooltip(user.created_at)}
-
- .col-md-4
- %h4 Latest groups
- %hr
- - @groups.each do |group|
+ = number_with_delimiter(Issue.count)
+ %p
+ Merge Requests
+ %span.light.pull-right
+ = number_with_delimiter(MergeRequest.count)
+ %p
+ Notes
+ %span.light.pull-right
+ = number_with_delimiter(Note.count)
+ %p
+ Snippets
+ %span.light.pull-right
+ = number_with_delimiter(Snippet.count)
+ %p
+ SSH Keys
+ %span.light.pull-right
+ = number_with_delimiter(Key.count)
+ %p
+ Milestones
+ %span.light.pull-right
+ = number_with_delimiter(Milestone.count)
+ %p
+ Active Users
+ %span.light.pull-right
+ = number_with_delimiter(User.active.count)
+ .col-md-4
+ %h4
+ Features
+ %hr
+ %p
+ Sign up
+ %span.light.pull-right
+ = boolean_to_icon signup_enabled?
%p
- = link_to [:admin, group], class: 'str-truncated' do
- = group.name
+ LDAP
%span.light.pull-right
- #{time_ago_with_tooltip(group.created_at)}
+ = boolean_to_icon Gitlab.config.ldap.enabled
+ %p
+ Gravatar
+ %span.light.pull-right
+ = boolean_to_icon gravatar_enabled?
+ %p
+ OmniAuth
+ %span.light.pull-right
+ = boolean_to_icon Gitlab.config.omniauth.enabled
+ %p
+ Reply by email
+ %span.light.pull-right
+ = boolean_to_icon Gitlab::IncomingEmail.enabled?
+ .col-md-4
+ %h4
+ Components
+ - if current_application_settings.version_check_enabled
+ .pull-right
+ = version_status_badge
+
+ %hr
+ %p
+ GitLab
+ %span.pull-right
+ = Gitlab::VERSION
+ %p
+ GitLab Shell
+ %span.pull-right
+ = Gitlab::Shell.new.version
+ %p
+ GitLab API
+ %span.pull-right
+ = API::API::version
+ %p
+ Git
+ %span.pull-right
+ = Gitlab::Git.version
+ %p
+ Ruby
+ %span.pull-right
+ #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
+
+ %p
+ Rails
+ %span.pull-right
+ #{Rails::VERSION::STRING}
+
+ %p
+ = Gitlab::Database.adapter_name
+ %span.pull-right
+ = Gitlab::Database.version
+ %hr
+ .row
+ .col-sm-4
+ .light-well
+ %h4 Projects
+ .data
+ = link_to admin_namespaces_projects_path do
+ %h1= number_with_delimiter(Project.count)
+ %hr
+ = link_to('New Project', new_project_path, class: "btn btn-new")
+ .col-sm-4
+ .light-well
+ %h4 Users
+ .data
+ = link_to admin_users_path do
+ %h1= number_with_delimiter(User.count)
+ %hr
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ .col-sm-4
+ .light-well
+ %h4 Groups
+ .data
+ = link_to admin_groups_path do
+ %h1= number_with_delimiter(Group.count)
+ %hr
+ = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
+
+ .row.prepend-top-10
+ .col-md-4
+ %h4 Latest projects
+ %hr
+ - @projects.each do |project|
+ %p
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ %span.light.pull-right
+ #{time_ago_with_tooltip(project.created_at)}
+
+ .col-md-4
+ %h4 Latest users
+ %hr
+ - @users.each do |user|
+ %p
+ = link_to [:admin, user], class: 'str-truncated' do
+ = user.name
+ %span.light.pull-right
+ #{time_ago_with_tooltip(user.created_at)}
+
+ .col-md-4
+ %h4 Latest groups
+ %hr
+ - @groups.each do |group|
+ %p
+ = link_to [:admin, group], class: 'str-truncated' do
+ = group.name
+ %span.light.pull-right
+ #{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 9025aaac097..59fd6c3fea0 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,28 +1,20 @@
- css_class = '' unless local_assigns[:css_class]
-- css_class += ' no-description' if group.description.blank?
-%li.group-row{ class: css_class }
- .controls.hidden-xs
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm'
- = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove'
+%li.group-row.group-admin{ class: css_class }
+ .group-avatar
+ = image_tag group_icon(group), class: 'avatar hidden-xs'
+ .group-details
+ .title
+ = link_to [:admin, group], class: 'group-name' do
+ = group.name
+ .group-stats
+ %span>= pluralize(number_with_delimiter(group.projects.count), 'project')
+ ,
+ %span= pluralize(number_with_delimiter(group.users.count), 'member')
- .stats
- %span
- = icon('bookmark')
- = number_with_delimiter(group.projects.count)
-
- %span
- = icon('users')
- = number_with_delimiter(group.users.count)
-
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
- = visibility_level_icon(group.visibility_level, fw: false)
-
- = image_tag group_icon(group), class: 'avatar s40 hidden-xs'
- .title
- = link_to [:admin, group], class: 'group-name' do
- = group.name
-
- - if group.description.present?
- .description
- = markdown(group.description, pipeline: :description)
+ - if group.description.present?
+ .description
+ = markdown(group.description, pipeline: :description)
+ .group-controls.hidden-xs
+ = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 775072a7441..794f910a61f 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,41 +1,35 @@
+- @no_container = true
- page_title "Groups"
-%h3.page-title
- Groups (#{number_with_delimiter(@groups.total_count)})
+= render "admin/dashboard/head"
-%p.light
- Group allows you to keep projects organized.
- Use groups for uniting related projects.
+%div{ class: container_class }
+ .top-area
+ .prepend-top-default.append-bottom-default
+ = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f|
+ = hidden_field_tag :sort, @sort
+ .search-holder
+ - project_name = params[:name].present? ? params[:name] : nil
+ .search-field-holder
+ = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
+ = icon("search", class: "search-icon")
+ .dropdown
+ - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li.dropdown-header
+ Sort by
+ %li
+ = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do
+ = sort_title_recently_created
+ = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do
+ = sort_title_oldest_created
+ = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do
+ = sort_title_recently_updated
+ = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
+ = sort_title_oldest_updated
+ = link_to new_admin_group_path, class: "btn btn-new" do
+ New Group
+ %ul.content-list
+ = render @groups
-.top-area
- .nav-search
- = form_tag admin_groups_path, method: :get, class: 'form-inline' do
- = hidden_field_tag :sort, @sort
- = text_field_tag :name, params[:name], class: "form-control"
- = button_tag "Search", class: "btn submit btn-primary"
-
- .nav-controls
- .dropdown.inline
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_groups_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to admin_groups_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to admin_groups_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to admin_groups_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
- = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
-
-%ul.content-list
- - @groups.each do |group|
- = render 'group', group: group
-
-= paginate @groups, theme: "gitlab"
+ = paginate @groups, theme: "gitlab"
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f309e80a39a..522153b37e3 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -88,28 +88,17 @@
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
+
+ = render 'shared/members/requests', membership_source: @group, requesters: @requesters
+
.panel.panel-default
.panel-heading
- %h3.panel-title
- Members
- %span.badge
- #{@group.group_members.count}
- %ul.well-list.group-users-list
- - @members.each do |member|
- - user = member.user
- %li{class: dom_class(member), id: (dom_id(user) if user)}
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = member.invite_email
- (invited)
- %span.pull-right.light
- = member.human_access
- - if can?(current_user, :destroy_group_member, member)
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-minus.fa-inverse
+ %strong= @group.name
+ group members
+ %span.badge= @group.members.size
+ .pull-right
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@group, :members]), class: "btn btn-xs"
+ %ul.well-list.group-users-list.content-list
+ = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
new file mode 100644
index 00000000000..e79303240f0
--- /dev/null
+++ b/app/views/admin/health_check/show.html.haml
@@ -0,0 +1,52 @@
+- @no_container = true
+- page_title "Health Check"
+= render 'admin/background_jobs/head'
+
+%div{ class: container_class }
+ %h3.page-title
+ Health Check
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ Access token is
+ %code#health-check-token= current_application_settings.health_check_access_token
+ = button_to reset_health_check_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset the health check token?' } do
+ = icon('refresh')
+ Reset health check access token
+ %p.light
+ Health information can be retrieved as plain text, JSON, or XML using:
+ %ul
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
+
+ %p.light
+ You can also ask for the status of specific services:
+ %ul
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
+
+ %hr
+ .panel.panel-default
+ .panel-heading
+ Current Status:
+ - if @errors.blank?
+ = icon('circle', class: 'cgreen')
+ Healthy
+ - else
+ = icon('warning', class: 'cred')
+ Unhealthy
+ .panel-body
+ - if @errors.blank?
+ No Health Problems Detected
+ - else
+ = @errors
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 698feb571ac..676812121d7 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,28 +1,32 @@
+- @no_container = true
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger]
-%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'
-.row-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' : ''),
- id: klass::file_name_noext }
- .file-holder#README
- .file-title
- %i.fa.fa-file
- = klass::file_name
- .pull-right
- = link_to '#', class: 'log-bottom' do
- %i.fa.fa-arrow-down
- Scroll down
- .file-content.logs
- %ol
- - klass.read_latest.each do |line|
- %li
- %p= line
+= render 'admin/background_jobs/head'
+
+%div{ class: container_class }
+ %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'
+ .row-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' : ''),
+ id: klass::file_name_noext }
+ .file-holder#README
+ .file-title
+ %i.fa.fa-file
+ = klass::file_name
+ .pull-right
+ = link_to '#', class: 'log-bottom' do
+ %i.fa.fa-arrow-down
+ Scroll down
+ .file-content.logs
+ %ol
+ - klass.read_latest.each do |line|
+ %li
+ %p= line
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index aa07afa0d62..7fbce25b2c4 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,94 +1,94 @@
+- @no_container = true
- page_title "Projects"
-= render 'shared/show_aside'
+- params[:visibility_level] ||= []
-.row.prepend-top-default
- %aside.col-md-3
- .panel.admin-filter
- = form_tag admin_namespaces_projects_path, method: :get, class: '' do
- .form-group
- = label_tag :name, 'Name:'
- = text_field_tag :name, params[:name], class: "form-control"
+= render "admin/dashboard/head"
- .form-group
- = label_tag :namespace_id, "Namespace"
- = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large'
+%div{ class: container_class }
+ .top-area
+ .prepend-top-default
+ = form_tag admin_namespaces_projects_path, method: :get do |f|
+ .search-holder
+ .search-field-holder
+ = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
- .form-group
- %strong Activity
- .checkbox
- = label_tag :with_push do
- = check_box_tag :with_push, 1, params[:with_push]
- %span Projects with push events
- .checkbox
- = label_tag :abandoned do
- = check_box_tag :abandoned, 1, params[:abandoned]
- %span No activity over 6 month
- .checkbox
- = label_tag :with_archived do
- = check_box_tag :with_archived, 1, params[:with_archived]
- %span Show archived projects
+ - if params[:visibility_level].present?
+ = hidden_field_tag 'visibility_level', params[:visibility_level]
- %fieldset
- %strong Visibility level:
- .visibility-levels
- - Project.visibility_levels.each do |label, level|
- .checkbox
- %label
- = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s)
- %span.descr
- = visibility_level_icon(level)
- = label
- %fieldset
- %strong Problems
- .checkbox
- = label_tag :last_repository_check_failed do
- = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
- %span Last repository check failed
+ - if params[:sort].present?
+ = hidden_field_tag 'sort', params[:sort]
- = hidden_field_tag :sort, params[:sort]
- = button_tag "Search", class: "btn submit btn-primary"
- = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
+ - if params[:personal].present?
+ = hidden_field_tag 'visibility_level', 'true'
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Projects (#{@projects.total_count})
- .controls
- .dropdown.inline
- %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
- = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do
- = sort_title_largest_repo
- = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success"
- %ul.well-list
- - @projects.each do |project|
- %li
- .list-item-name
- %span{ class: visibility_level_color(project.visibility_level) }
- = visibility_level_icon(project.visibility_level)
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- .pull-right
+ - if params[:archived].present?
+ = hidden_field_tag 'archived', 'true'
+
+ = icon("search", class: "search-icon")
+
+ .dropdown
+ - toggle_text = 'Search for Namespace'
+ - if params[:namespace_id].present?
+ - namespace = Namespace.find(params[:namespace_id])
+ - toggle_text = "#{namespace.kind}: #{namespace.path}"
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right
+ = dropdown_title('Namespaces')
+ = dropdown_filter("Search for Namespace")
+ = dropdown_content
+ = dropdown_loading
+
+ = button_tag "Search", class: "btn btn-primary btn-search"
+
+ %ul.nav-links
+ - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+ = nav_link(opts) do
+ = link_to admin_namespaces_projects_path do
+ All
+
+ = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
+ = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+ Private
+ = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
+ = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+ Internal
+ = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
+ = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+ Public
+
+ .nav-controls
+ = render 'shared/projects/dropdown'
+ = link_to new_project_path, class: 'btn btn-new' do
+ New Project
+
+ .projects-list-holder
+ - if @projects.any?
+ %ul.projects-list.content-list
+ - @projects.each_with_index do |project|
+ %li.project-row
+ .controls.pull-right
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
= repository_size(project)
- = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
- = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove"
- - if @projects.blank?
- .nothing-here-block 0 projects matches
- = paginate @projects, theme: "gitlab"
+ = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
+ = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
+ .title
+ = link_to [:admin, project.namespace.becomes(Namespace), project] do
+ .dash-project-avatar
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
+ - if project.description.present?
+ .description
+ = markdown(project.description, pipeline: :description)
+
+ = paginate @projects, theme: 'gitlab'
+ - else
+ .nothing-here-block No projects found
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 73986d21bcf..2c5aba71699 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -99,7 +99,13 @@
.form-group
= f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10
- = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large'
+ .dropdown
+ = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' })
+ .dropdown-menu.dropdown-select
+ = dropdown_title('Namespaces')
+ = dropdown_filter("Search for Namespace")
+ = dropdown_content
+ = dropdown_loading
.form-group
.col-sm-offset-2.col-sm-10
@@ -135,44 +141,27 @@
- if @group
.panel.panel-default
.panel-heading
- %strong #{@group.name}
- group members (#{@group.group_members.count})
+ %strong= @group.name
+ group members
+ %span.badge= @group_members.size
.pull-right
= link_to admin_group_path(@group), class: 'btn btn-xs' do
- %i.fa.fa-pencil-square-o
- %ul.well-list
- - @group_members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
+ = icon('pencil-square-o', text: 'Manage Access')
+ %ul.well-list.content-list
+ = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
.panel-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+
.panel.panel-default
.panel-heading
- Project members
- %small
- (#{@project.users.count})
+ %strong= @project.name
+ project members
+ %span.badge= @project.users.size
.pull-right
- = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do
- %i.fa.fa-pencil-square-o
- Manage Access
- %ul.well-list.project_members
- - @project_members.each do |project_member|
- - user = project_member.user
- %li.project_member
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = project_member.invite_email
- (invited)
- .pull-right
- - if project_member.owner?
- %span.light Owner
- - else
- %span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
- %i.fa.fa-times
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@project, :members]), class: "btn btn-xs"
+ %ul.well-list.project_members.content-list
+ = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
.panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 36b21eefdee..64893b38c58 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -4,6 +4,8 @@
%span.label.label-success shared
- else
%span.label.label-info specific
+ - if runner.locked?
+ %span.label.label-warning locked
- unless runner.active?
%span.label.label-danger paused
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2dad64b8d0f..a53876d6757 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,67 +1,76 @@
-%p.lead.prepend-top-default
- %span
- To register a new runner you should enter the following registration token.
- With this token the runner will request a unique runner token and use that for future communication.
- Registration token is
- %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
+- @no_container = true
+= render "admin/dashboard/head"
-.bs-callout.clearfix
- .pull-left
- %p
- You can reset runners registration token by pressing a button below.
- %p
- = button_to reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset registration token?' } do
- = icon('refresh')
- Reset runners registration token
+%div{ class: container_class }
+
+ %p.prepend-top-default
+ %span
+ To register a new runner you should enter the following registration token.
+ With this token the runner will request a unique runner token and use that for future communication.
+ %br
+ Registration token is
+ %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
-.bs-callout
- %p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
- %br
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset runners registration token by pressing a button below.
+ %p
+ = button_to reset_runners_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset registration token?' } do
+ = icon('refresh')
+ Reset runners registration token
+
+ .bs-callout
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+ %br
- %div
- %span Each runner can be in one of the following states:
- %ul
- %li
- %span.label.label-success shared
- \- run builds from all unassigned projects
- %li
- %span.label.label-info specific
- \- run builds from assigned projects
- %li
- %span.label.label-danger paused
- \- runner will not receive any new builds
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-warning locked
+ \- runner cannot be assigned to other projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new builds
-.append-bottom-20.clearfix
- .pull-left
- = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ .append-bottom-20.clearfix
+ .pull-left
+ = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = submit_tag 'Search', class: 'btn'
- .pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
-%br
+ %br
-.table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Projects
- %th Builds
- %th Tags
- %th Last contact
- %th
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
-= paginate @runners
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 8700b4820cd..61abfc6ecbe 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -9,8 +9,6 @@
%span.runner-state.runner-state-specific
Specific
-
-
- if @runner.shared?
.bs-callout.bs-callout-success
%h4 This runner will process builds from ALL UNASSIGNED projects
@@ -22,31 +20,15 @@
%h4 This runner will process builds only from ASSIGNED projects
%p You can't make this a shared runner.
%hr
-= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
- .form-group
- = label_tag :token, class: 'control-label' do
- Token
- .col-sm-10
- = f.text_field :token, class: 'form-control', readonly: true
- .form-group
- = label_tag :description, class: 'control-label' do
- Description
- .col-sm-10
- = f.text_field :description, class: 'form-control'
- .form-group
- = label_tag :tag_list, class: 'control-label' do
- Tags
- .col-sm-10
- = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control'
- .help-block You can setup builds to only use runners with specific tags
- .form-actions
- = f.submit 'Save', class: 'btn btn-save'
+
+.append-bottom-20
+ = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
.row
.col-md-6
%h4 Restrict projects for this runner
- if @runner.projects.any?
- %table.table
+ %table.table.assigned-projects
%thead
%tr
%th Assigned projects
@@ -62,7 +44,7 @@
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
- %table.table
+ %table.table.unassigned-projects
%thead
%tr
%th Project
@@ -117,8 +99,8 @@
%td.build-link
- if project
- = link_to ci_status_path(build.commit) do
- %strong #{build.commit.short_sha}
+ = link_to ci_status_path(build.pipeline) do
+ %strong #{build.pipeline.short_sha}
%td.timestamp
- if build.finished_at
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
new file mode 100644
index 00000000000..6956e5ab795
--- /dev/null
+++ b/app/views/admin/system_info/show.html.haml
@@ -0,0 +1,25 @@
+- @no_container = true
+- page_title "System Info"
+= render 'admin/background_jobs/head'
+
+%div{ class: container_class }
+ .prepend-top-default
+ .row
+ .col-sm-4
+ .light-well
+ %h4 CPU
+ .data
+ %h1= "#{@cpus} cores"
+ .col-sm-4
+ .light-well
+ %h4 Memory
+ .data
+ %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}"
+ .col-sm-4
+ .light-well
+ %h4 Disks
+ .data
+ - @disks.each do |disk|
+ %h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}"
+ %p= "#{disk[:disk_name]}"
+ %p= "#{disk[:mount_path]}"
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
new file mode 100644
index 00000000000..d3519f616f6
--- /dev/null
+++ b/app/views/admin/users/_user.html.haml
@@ -0,0 +1,42 @@
+%li.user-row
+ .user-avatar
+ = image_tag avatar_icon(user), class: "avatar", alt: ''
+ .user-details
+ .user-name
+ = link_to user.name, [:admin, user]
+ - if user.blocked?
+ %span.label.label-danger blocked
+ - if user.admin?
+ %span.label.label-success Admin
+ - if user.external?
+ %span.label.label-default External
+ - if user == current_user
+ %span It's you!
+ .user-email
+ = mail_to user.email, user.email
+ .controls.pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
+ - unless user == current_user
+ .dropdown.inline
+ %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } }
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li.dropdown-header
+ Settings
+ %li
+ - if user.ldap_blocked?
+ %span.small Cannot unblock LDAP blocked users
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put
+ - else
+ = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
+ - if user.access_locked?
+ %li
+ = 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?
+ %li.divider
+ %li
+ = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
+ class: 'btn btn-remove btn-block',
+ method: :delete
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
index dbecb7bbfd6..8f6d13b881a 100644
--- a/app/views/admin/users/groups.html.haml
+++ b/app/views/admin/users/groups.html.haml
@@ -1,11 +1,12 @@
- page_title "Groups", @user.name, "Users"
= render 'admin/users/head'
-- if @user.group_members.present?
+- group_members = @user.group_members.includes(:source)
+- if group_members.any?
.panel.panel-default
.panel-heading Groups:
%ul.well-list
- - @user.group_members.each do |group_member|
+ - group_members.each do |group_member|
- group = group_member.group
%li.group_member
%span{class: ("list-item-name" unless group_member.owner?)}
@@ -13,7 +14,7 @@
.pull-right
%span.light= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index d6743081c8e..357123c2c13 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,107 +1,78 @@
+- @no_container = true
- page_title "Users"
-= render 'shared/show_aside'
+= render "admin/dashboard/head"
-.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)
+%div{ class: container_class }
+ .top-area
+ .prepend-top-default
+ = form_tag admin_users_path, method: :get do
+ - if params[:filter].present?
+ = hidden_field_tag "filter", h(params[:filter])
+ .search-holder
+ .search-field-holder
+ = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+ = icon("search", class: "search-icon")
+ .dropdown
+ - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li.dropdown-header
+ Sort by
+ %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-search'
- .row-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
- = 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
+ .nav-block
+ %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
+ .fade-left
+ = nav_link(html_options: { class: ('active' unless params[:filter]) }) do
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
+ = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
+ .fade-right
- = 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|
+ %ul.users-list.content-list
+ - if @users.empty?
%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
- = 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"
+ .nothing-here-block No users found.
+ - else
+ = render partial: 'admin/users/user', collection: @users
+
+ = paginate @users, theme: "gitlab"
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index b655b2a15f5..84b9ceb23b3 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -38,6 +38,5 @@
%span.light= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
-
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
new file mode 100644
index 00000000000..02efcecc889
--- /dev/null
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -0,0 +1,15 @@
+- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+ - awards_sort(grouped_emojis).each do |emoji, awards|
+ %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
+ = emoji_icon(emoji, sprite: false)
+ %span.award-control-text.js-counter
+ = awards.count
+
+ - if current_user
+ .award-menu-holder.js-award-holder
+ %button.btn.award-control.js-add-award{ type: "button" }
+ = icon('smile-o', class: "award-control-icon award-control-icon-normal")
+ = icon('spinner spin', class: "award-control-icon award-control-icon-loading")
+ %span.award-control-text
+ Add
diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml
deleted file mode 100644
index 2788112c835..00000000000
--- a/app/views/ci/errors/show.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%h3.error Error
-= @error
diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml
deleted file mode 100644
index 09e7e653521..00000000000
--- a/app/views/ci/shared/_guide.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.bs-callout.help-callout
- %h4 How to setup CI for this project
-
- %ol
- %li
- Add at least one runner to the project.
- Go to #{link_to 'Runners page', runners_path(@project), target: :blank} for instructions.
- %li
- Put the .gitlab-ci.yml in the root of your repository. Examples can be found in
- #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}.
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
- %li
- Return to this page and refresh it, it should show a new build.
diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml
deleted file mode 100644
index f56c37d9b37..00000000000
--- a/app/views/ci/shared/_no_runners.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.alert.alert-danger
- %p
- Now you need Runners to process your builds.
- %span
- Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
-
-
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 3d17f74b709..23c145ebbb4 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -9,5 +9,4 @@
- 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 9da3fcbd986..f7abad54286 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -13,10 +13,9 @@
Explore Projects
.nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = form_tag request.path, 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-new' do
- = icon('plus')
New Project
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 0d7b1b30dc3..0404d0728ea 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,10 +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.xmlschema if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- @issues.each do |issue|
- issue_to_atom(xml, issue)
- end
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
-
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index d4daf07c6c0..fb5be63b472 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.id dashboard_projects_url
xml.updated @events[0].updated_at.xmlschema if @events[0]
- @events.each do |event|
- event_to_atom(xml, event)
- end
+ xml << render(partial: 'events/event', collection: @events) if @events.any?
end
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index aa0aff86d4d..98f302d2f93 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,13 +1,15 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
-
.todo-title.title
- %span.author-name
- - if todo.author
- = link_to_author(todo)
- - else
- (removed)
+ - unless todo.build_failed?
+ = todo_target_state_pill(todo)
+
+ %span.author-name
+ - if todo.author
+ = link_to_author(todo)
+ - else
+ (removed)
%span.todo-label
= todo_action_name(todo)
- if todo.target
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 3c3830a3f10..73c3a3dd2eb 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,6 +3,9 @@
Almost there...
%p.lead
Please check your email to confirm your account
+- if after_sign_up_text.present?
+ .well-confirmation.text-center
+ = markdown(after_sign_up_text)
%p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb
deleted file mode 100644
index c6fa8f0ee36..00000000000
--- a/app/views/devise/mailer/confirmation_instructions.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-<p>Welcome <%= @resource.name %>!</p>
-
-<% if @resource.unconfirmed_email.present? %>
- <p>You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:</p>
-<% else %>
- <p>You can confirm your account through the link below:</p>
-<% end %>
-
-<p><%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %></p>
diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml
new file mode 100644
index 00000000000..086bb8e083d
--- /dev/null
+++ b/app/views/devise/mailer/confirmation_instructions.html.haml
@@ -0,0 +1,16 @@
+.center
+ - if @resource.unconfirmed_email.present?
+ #content
+ %h2= @resource.unconfirmed_email
+ %p Click the link below to confirm your email address.
+ #cta
+ = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
+ - else
+ #content
+ - if Gitlab.com?
+ %h2 Thanks for signing up to GitLab!
+ - else
+ %h2 Welcome, #{@resource.name}!
+ %p To get started, click the link below to confirm your account.
+ #cta
+ = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token)
diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb
new file mode 100644
index 00000000000..9f76edb76a4
--- /dev/null
+++ b/app/views/devise/mailer/confirmation_instructions.text.erb
@@ -0,0 +1,9 @@
+Welcome, <%= @resource.name %>!
+
+<% if @resource.unconfirmed_email.present? %>
+You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:
+<% else %>
+You can confirm your account through the link below:
+<% end %>
+
+<%= confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/views/devise/mailer/password_change.html.haml b/app/views/devise/mailer/password_change.html.haml
new file mode 100644
index 00000000000..3349ee84807
--- /dev/null
+++ b/app/views/devise/mailer/password_change.html.haml
@@ -0,0 +1,10 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ The password for your GitLab account on
+ #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}
+ has successfully been changed.
+ %p
+ If you did not initiate this change, please contact your administrator
+ immediately.
diff --git a/app/views/devise/mailer/password_change.text.erb b/app/views/devise/mailer/password_change.text.erb
new file mode 100644
index 00000000000..95923d9f8de
--- /dev/null
+++ b/app/views/devise/mailer/password_change.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+The password for your GitLab account on <%= Gitlab.config.gitlab.url %>
+has successfully been changed.
+
+If you did not initiate this change, please contact your administrator
+immediately.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
deleted file mode 100644
index 23b31da92d8..00000000000
--- a/app/views/devise/mailer/reset_password_instructions.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-<p>Hello <%= @resource.email %>!</p>
-
-<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
-
-<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
-
-<p>If you didn't request this, please ignore this email.</p>
-<p>Your password won't change until you access the link above and create a new one.</p>
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml
new file mode 100644
index 00000000000..e91c9522520
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.haml
@@ -0,0 +1,12 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Someone, hopefully you, has requested to reset the password for your
+ GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
+ %p
+ If you did not perform this request, you can safely ignore this email.
+ %p
+ Otherwise, click the link below to complete the process.
+ #cta
+ = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb
new file mode 100644
index 00000000000..116313ee11c
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.text.erb
@@ -0,0 +1,10 @@
+Hello, <%= @resource.name %>!
+
+Someone, hopefully you, has requested to reset the password for your GitLab
+account on <%= Gitlab.config.gitlab.url %>
+
+If you did not perform this request, you can safely ignore this email.
+
+Otherwise, click the link below to complete the process:
+
+<%= edit_password_url(@resource, reset_password_token: @token) %>
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 52b327e20c5..9990d1ccac6 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -1,10 +1,9 @@
-%p
-Hello #{@resource.name}!
-
-%p
- Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in
- = time_ago_in_words(Devise.unlock_in.from_now)
- or you may click the link below to unlock now.
-
-%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token)
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Your GitLab account has been locked due to an excessive amount of unsuccessful
+ sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ or you may click the link below to unlock now.
+ #cta
+ = link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
new file mode 100644
index 00000000000..3aea3e20145
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+Your GitLab account has been locked due to an excessive amount of unsuccessful
+sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+or you may click the link below to unlock now.
+
+<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index d65fa60025c..28194506acc 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -4,7 +4,7 @@
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if omniauth_enabled? && devise_mapping.omniauthable?
+ - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index c9d1e454a5e..a373f61bd3c 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,10 +1,19 @@
%div
.login-box
.login-heading
- %h3 Two-factor Authentication
+ %h3 Two-Factor Authentication
.login-body
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
- = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true
- %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
- .prepend-top-20
- = f.submit "Verify code", class: "btn btn-save"
+ - if @user.two_factor_otp_enabled?
+ %h5 Authenticate via Two-Factor App
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
+ - resource_params = params[resource_name].presence || params
+ = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
+ = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
+ %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
+ .prepend-top-20
+ = f.submit "Verify code", class: "btn btn-save"
+
+ - if @user.two_factor_u2f_enabled?
+
+ %hr
+ = render "u2f/authenticate"
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index ecf680e7b23..de18bc2d844 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,7 +1,7 @@
%p
%span.light
Sign in with &nbsp;
- - providers = button_based_providers
+ - providers = enabled_button_based_providers
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 510215bb8cd..905a8dbcd84 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -16,7 +16,7 @@
%div
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
- = f.password_field :password, class: "form-control bottom", placeholder: "Password", required: true
+ = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters"
%div
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index eae80e5210f..ce050007204 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title Authorize required
+%h3.page-title Authorization required
%main{:role => "main"}
%p.h4
Authorize
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index 3443a8e2307..8b38b4c2bd4 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -1,9 +1,9 @@
.emoji-menu
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Seach emojis"
.emoji-menu-content
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- - AwardEmoji.emoji_by_category.each do |category, emojis|
+ - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title
- = AwardEmoji::CATEGORIES[category]
+ = Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index dce4081288c..1bc9f604438 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -2,4 +2,4 @@
.commit-row-title
= 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
+ = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
new file mode 100644
index 00000000000..7890e717aa7
--- /dev/null
+++ b/app/views/events/_event.atom.builder
@@ -0,0 +1,20 @@
+return unless event.visible_to_user?(current_user)
+
+xml.entry do
+ xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
+ xml.link href: event_feed_url(event)
+ xml.title truncate(event_feed_title(event), length: 80)
+ xml.updated event.created_at.xmlschema
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
+
+ xml.author do
+ xml.name event.author_name
+ xml.email event.author_email
+ end
+
+ xml.summary(type: "xhtml") do |summary|
+ event_summary = event_feed_summary(event)
+
+ summary << event_summary unless event_summary.nil?
+ end
+end
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index fad65310021..083c3936212 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown(issue.description, pipeline: :atom, project: issue.project)
+ = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author)
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index 19bdc7b9ca5..d7e05600627 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown(merge_request.description, pipeline: :atom, project: merge_request.project)
+ = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index b730ebbd5f9..1154f982821 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown(note.note, pipeline: :atom, project: note.project)
+ = markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index b271b9daff1..28bee1d0a33 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
%i
at
= commit[:timestamp].to_time.to_s(:short)
- %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project)
+ %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author)
- if event.commits_count > 15
%p
%i
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index c994e3b997d..2e2403347c1 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,10 +1,14 @@
.event-title
%span.author_name= link_to_author event
%span.event_label{class: event.action_name}
- = event_action_name(event)
-
- if event.target
- %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target]
+ = event.action_name
+ %strong
+ = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do
+ = event.target_type.titleize.downcase
+ = event.target.reference_link_text
+ - else
+ = event_action_name(event)
= event_preposition(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 235bd46107e..ea54ef226ec 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -1,3 +1,5 @@
+- project = event.project
+
.event-title
%span.author_name= link_to_author event
%span.event_label.pushed #{event.action_name} #{event.ref_type}
@@ -5,19 +7,18 @@
%strong= event.ref_name
- else
%strong
- = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
+ = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title)
at
- = link_to_project event.project
+ = link_to_project project
- if event.push_with_commits?
- - project = event.project
.event-body
%ul.well-list.event_commits
- few_commits = event.commits[0...2]
- few_commits.each do |commit|
- = render "events/commit", commit: commit, project: project
+ = render "events/commit", commit: commit, project: project, event: event
- - create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project)
+ - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
@@ -27,18 +28,26 @@
- from = event.commit_from
- from_label = truncate_sha(from)
- else
- - from = event.project.default_branch
+ - from = project.default_branch
- from_label = from
- = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do
+ = link_to namespace_project_compare_path(project.namespace, project, from: from, to: event.commit_to) do
Compare #{from_label}...#{truncate_sha(event.commit_to)}
- if create_mr
%span{"data-user-is" => event.author_id, "data-display" => "inline"}
or
- = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do
+ = link_to create_mr_path(project.default_branch, event.ref_name, project) do
create a merge request
- elsif create_mr
%li.commits-stat{"data-user-is" => event.author_id}
- = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do
+ = link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request
+- elsif event.rm_ref?
+ - repository = project.repository
+ - last_commit = repository.commit(event.commit_from)
+ - if last_commit
+ .event-body
+ %ul.well-list.event_commits
+ = render "events/commit", commit: last_commit, project: project, event: event
+
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index dc76599b776..71cc4d87b1f 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -4,7 +4,7 @@
.nav-block
- if current_user
.controls
- = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ = link_to group_path(@group, format: :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/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
deleted file mode 100644
index 60234be8f83..00000000000
--- a/app/views/groups/group_members/_group_member.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-- 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)}
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if show_controls && can?(current_user, :admin_group_member, @group)
- = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
-
- - if show_roles && should_user_see_group_roles?(current_user, @group)
- %span.pull-right
- %strong.member-access-level= member.human_access
- - if show_controls
- - if can?(current_user, :update_group_member, member)
- = button_tag class: "btn-xs btn js-toggle-button",
- title: 'Edit access level', type: 'button' do
- %i.fa.fa-pencil-square-o
-
- - if can?(current_user, :destroy_group_member, member)
- &nbsp;
- - if current_user == user
- = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- = icon("sign-out")
- Leave
- - else
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-minus.fa-inverse
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for [@group, member], remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 0eb6bbd4420..90f362c052b 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,23 +1,23 @@
- page_title "Members"
.group-members-page.prepend-top-default
- - if current_user && current_user.can?(:admin_group_member, @group)
+ - if can?(current_user, :admin_group_member, @group)
.panel.panel-default
.panel-heading
Add new user to group
.panel-body
- - if should_user_see_group_roles?(current_user, @group)
- %p.light
- Members of group have access to all group projects.
+ %p.light
+ Members of group have access to all group projects.
.new-group-member-holder
= render "new_group_member"
+ = render 'shared/members/requests', membership_source: @group, requesters: @requesters
+
.panel.panel-default
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{@members.total_count})
+ %span.badge= @members.size
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
@@ -25,8 +25,7 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - @members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: true
+ = render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
:javascript
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index df726e2b2b9..da71de4cd1e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 486d1d8587a..b1628040325 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,13 +1,10 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "#{@user.name} issues"
- 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.xmlschema if @issues.any?
+ xml.title "#{@group.name} issues"
+ xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: issues_group_url, rel: "alternate", type: "text/html"
+ xml.id issues_group_url
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- @issues.each do |issue|
- issue_to_atom(xml, issue)
- end
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
-
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 7d9d27ae1fc..ca6c4326d1c 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -39,9 +39,8 @@
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
- .col-sm-10= f.hidden_field :due_date
.col-sm-10
- .datepicker
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
.form-actions
= f.submit 'Create Milestone', class: "btn-create btn"
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index c66b82bb484..b68bf444d27 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.id group_url(@group)
xml.updated @events[0].updated_at.xmlschema if @events[0]
- @events.each do |event|
- event_to_atom(xml, event)
- end
+ xml << render(@events) if @events.any?
end
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 77c297255b8..a83eb7e88bb 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -5,7 +5,7 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block.groups-cover-block
- .container-fluid.container-limited
+ %div{ class: container_class }
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s70"
.group-info
@@ -15,6 +15,11 @@
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
+ .group-right-buttons.btn-group
+ - if current_user
+ .pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
+
- if @group.description.present?
.cover-desc.description
= markdown(@group.description, pipeline: :description)
@@ -30,12 +35,11 @@
= 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|
+ = form_tag request.path, 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
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 70e88da7aae..8cc0b59edeb 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -24,12 +24,16 @@
%td Show/hide this dialog
%tr
%td.shortcut
- - if browser.mac?
+ - if browser.platform.mac?
.key &#8984; shift p
- else
.key ctrl shift p
-
%td Toggle Markdown preview
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Edit last comment (when focused on an empty textarea)
%tbody
%tr
%th
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 57bc91ea5a9..57601ae9be0 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -36,6 +36,6 @@
%ul.well-list
%li= link_to 'See our website for getting help', promo_url + '/getting-help/'
%li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)'
- %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.showHelp(event)'
+ %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()'
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index dfebf7768d9..804ad88468f 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -1,6 +1,8 @@
- if @already_been_taken
:plain
- target_field = $("tr#repo_#{@repo_id} .import-target")
+ tr = $("tr#repo_#{@repo_id}")
+ target_field = tr.find(".import-target")
+ import_button = tr.find(".btn-import")
origin_target = target_field.text()
project_name = "#{@project_name}"
origin_namespace = "#{@target_namespace}"
@@ -10,6 +12,7 @@
target_field.append("/" + project_name)
target_field.data("project_name", project_name)
target_field.find('input').prop("value", origin_namespace)
+ import_button.enable().removeClass('is-loading')
- elsif @access_denied
:plain
job = $("tr#repo_#{@repo_id}")
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
new file mode 100644
index 00000000000..435ed7bd4cb
--- /dev/null
+++ b/app/views/import/github/new.html.haml
@@ -0,0 +1,43 @@
+- page_title "GitHub Import"
+- header_title "Projects", root_path
+
+%h3.page-title
+ = icon 'github', text: 'Import Projects from GitHub'
+
+- if github_import_configured?
+ %p
+ To import a GitHub project, you first need to authorize GitLab to access
+ the list of your GitHub repositories:
+
+ = link_to 'List Your GitHub Repositories', status_import_github_path, class: 'btn btn-success'
+
+ %hr
+
+%p
+ - if github_import_configured?
+ Alternatively,
+ - else
+ To import a GitHub project,
+ you can use a
+ = succeed '.' do
+ = link_to 'Personal Access Token', 'https://github.com/settings/tokens'
+ When you create your Personal Access Token,
+ you will need to select the <code>repo</code> scope, so we can display a
+ list of your public and private repositories which are available for import.
+
+= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do
+ .form-group
+ = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40
+ = submit_tag 'List Your GitHub Repositories', class: 'btn btn-success'
+
+- unless github_import_configured?
+ %hr
+ %p
+ Note:
+ - if current_user.admin?
+ As an administrator you may like to configure
+ - else
+ Consider asking your GitLab administrator to configure
+ = link_to 'GitHub integration', help_page_path("integration", "github")
+ which will allow login via GitHub and allow importing projects without
+ generating a Personal Access Token.
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 5b7f11440c1..7486b1423e2 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -4,6 +4,10 @@
%i.fa.fa-github
Import projects from GitHub
+%p
+ %i.fa.fa-warning
+ To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
+
%p.light
Select projects you want to import.
%hr
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index e3a356b5379..aedb8468eca 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -47,7 +47,7 @@
%td.import-target
= repo["path_with_namespace"]
%td.import-actions.job-status
- = button_tag class: "btn js-add-to-import" do
+ = button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
new file mode 100644
index 00000000000..44e2653ca4a
--- /dev/null
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -0,0 +1,25 @@
+- page_title "GitLab Import"
+- header_title "Projects", root_path
+%h3.page-title
+ = icon('gitlab')
+ Import an exported GitLab project
+%hr
+
+= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do
+ %p
+ Project will be imported as
+ %strong
+ #{@namespace_name}/#{@path}
+
+ %p
+ To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
+ .form-group
+ = hidden_field_tag :namespace_id, @namespace_id
+ = hidden_field_tag :path, @path
+ = label_tag :file, class: 'control-label' do
+ %span GitLab project export
+ .col-sm-10
+ = file_field_tag :file, class: ''
+
+ .form-actions
+ = submit_tag 'Import project', class: 'btn btn-create'
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
new file mode 100644
index 00000000000..96831874144
--- /dev/null
+++ b/app/views/issues/_issue.atom.builder
@@ -0,0 +1,32 @@
+xml.entry do
+ xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue)
+ 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.xmlschema
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
+
+ xml.author do
+ xml.name issue.author_name
+ xml.email issue.author_email
+ end
+
+ xml.summary issue.title
+ xml.description issue.description if issue.description
+ xml.milestone issue.milestone.title if issue.milestone
+ xml.due_date issue.due_date if issue.due_date
+
+ unless issue.labels.empty?
+ xml.labels do
+ issue.labels.each do |label|
+ xml.label label.name
+ end
+ end
+ end
+
+ if issue.assignee
+ xml.assignee do
+ xml.name issue.assignee.name
+ xml.email issue.assignee.email
+ end
+ end
+end
diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml
index ada7306d98d..e7a70e3bb28 100644
--- a/app/views/kaminari/gitlab/_first_page.html.haml
+++ b/app/views/kaminari/gitlab/_first_page.html.haml
@@ -2,7 +2,7 @@
-# available local variables
-# url: url to the first page
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
%li.first
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 3ffd12f8587..80ca30f36e6 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -1,7 +1,7 @@
-# Non-link tag that stands for skipped pages...
-# available local variables
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
%li{class: "page"}
diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml
index 3431d029bcc..53f780d1d1b 100644
--- a/app/views/kaminari/gitlab/_last_page.html.haml
+++ b/app/views/kaminari/gitlab/_last_page.html.haml
@@ -2,7 +2,7 @@
-# available local variables
-# url: url to the last page
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
%li.last
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index c805914fc3f..125f09777ba 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -2,7 +2,7 @@
-# available local variables
-# url: url to the next page
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.last?
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index a52d883b9a8..522e4d1d05f 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -3,7 +3,7 @@
-# page: a page object for "this" page
-# url: url to this page
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
%li{class: "page#{' active' if page.current?}"}
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index a12c53bcfe7..f5e0d2ed3f3 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -1,7 +1,7 @@
-# The container tag
-# available local variables
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-# paginator: the paginator that renders the pagination tags inside
@@ -9,7 +9,7 @@
%div.gl-pagination
%ul.pagination.clearfix
- unless current_page.first?
- = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages
+ = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
= prev_page_tag
- each_page do |page|
- if page.left_outer? || page.right_outer? || page.inside_window?
@@ -18,5 +18,5 @@
= gap_tag
= next_page_tag
- unless current_page.last?
- = last_page_tag unless num_pages < 5
+ = last_page_tag unless total_pages < 5
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index afb20455e0a..7edf10498a8 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -2,7 +2,7 @@
-# available local variables
-# url: url to the previous page
-# current_page: a page object for the currently displayed page
--# num_pages: total number of pages
+-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.first?
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
index 2ed51d87ca1..8c140a5943e 100644
--- a/app/views/layouts/_collapse_button.html.haml
+++ b/app/views/layouts/_collapse_button.html.haml
@@ -1,4 +1,3 @@
-- if nav_menu_collapsed?
- = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
-- else
- = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
+= link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do
+ %span.sr-only Toggle navigation
+ = icon('bars')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 79cdbac1f37..757de92d6d4 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -30,9 +30,10 @@
= javascript_include_tag "application"
- = csrf_meta_tags
+ - if content_for?(:page_specific_javascripts)
+ = yield :page_specific_javascripts
- = include_gon
+ = csrf_meta_tags
- unless browser.safari?
%meta{name: 'referrer', content: 'origin-when-cross-origin'}
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 96b38485425..12e7ed0e792 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,4 +3,5 @@
- if @noteable
:javascript
GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.cachedData = undefined;
GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 3c3bc41bf0e..8596bbfdef6 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,13 +1,6 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
- = render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
- .header-logo
- %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
-
+ = render partial: 'layouts/collapse_button'
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
@@ -15,18 +8,15 @@
- else
= render 'layouts/nav/explore'
- .collapse-nav
- = render partial: 'layouts/collapse_button'
- - if current_user
- = link_to current_user, class: 'sidebar-user', title: "Profile" do
- = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
- .username
- = current_user.username
+ = link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
+ %span.sr-only Toggle navigation pinning
+ = icon('thumb-tack')
- if defined?(nav) && nav
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
- .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) }
+ .content-wrapper{ class: "#{layout_nav_class}" }
+ = render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
%div{ class: (container_class unless @no_container) }
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 6b208c3d0bb..245b9c3b4d4 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -6,11 +6,8 @@
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- .search-location-badge
- - if label.present?
- %span.location-badge
- %i.location-text
- = label
+ - if label.present?
+ .location-badge= label
.search-input-wrap
.dropdown{ data: {url: search_autocomplete_path } }
= search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
@@ -39,6 +36,31 @@
- else
= hidden_field_tag :search_code, true
+ :javascript
+ gl.projectOptions = gl.projectOptions || {};
+ gl.projectOptions["#{j(@project.path)}"] = {
+ issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
+ mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
+ name: "#{j(@project.name)}"
+ };
+
+ - if @group and @group.path
+ :javascript
+ gl.groupOptions = gl.groupOptions || {};
+ gl.groupOptions["#{j(@group.path)}"] = {
+ name: "#{j(@group.name)}",
+ issuesPath: "#{issues_group_path(j(@group.path))}",
+ mrPath: "#{merge_requests_group_path(j(@group.path))}"
+ };
+
+
+ :javascript
+ gl.dashboardOptions = {
+ issuesPath: "#{issues_dashboard_url}",
+ mrPath: "#{merge_requests_dashboard_url}"
+ };
+
+
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 6591c52bdbd..87064cc9b3f 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,5 +1,5 @@
- page_title "Admin Area"
- header_title "Admin Area", admin_root_path
-- sidebar "admin"
+- nav "admin"
= render template: "layouts/application"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index e4d1c773d03..33cedaaf2ee 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,9 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body{class: "#{user_application_theme}", 'data-page' => body_data_page}
+ %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
+ = Gon::Base.render_data
+
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top
diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml
deleted file mode 100644
index 24c68a6dbf5..00000000000
--- a/app/views/layouts/ci/_info.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- if current_user && current_user.is_admin? && Ci::Runner.count.zero?
- = render 'ci/shared/no_runners'
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
deleted file mode 100644
index a13241bebee..00000000000
--- a/app/views/layouts/ci/_page.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-.page-with-sidebar{ class: page_sidebar_class }
- = render "layouts/broadcast"
- .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
- .header-logo
- %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
-
- - if defined?(sidebar) && sidebar
- = render "layouts/ci/#{sidebar}"
- - elsif current_user
- = render 'layouts/nav/dashboard'
- .collapse-nav
- = render partial: 'layouts/collapse_button'
- - if current_user
- = link_to current_user, class: 'sidebar-user', title: "Profile" do
- = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
- .username
- = current_user.username
- .content-wrapper
- = render "layouts/flash"
- = render 'layouts/ci/info'
- %div{ class: container_class }
- .content
- .clearfix
- = yield
diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml
deleted file mode 100644
index 270b206df5e..00000000000
--- a/app/views/layouts/ci/notify.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-%html{lang: "en"}
- %head
- %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
- %title
- GitLab CI
-
- %body
- = yield :header
-
- %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"}
- %tr
- %td{align: "left", style: "margin: 0; padding: 10px;"}
- = yield
- %br
- %tr
- %td{align: "left", style: "margin: 0; padding: 10px;"}
- %p{style: "font-size:small;color:#777"}
- - if @project
- You're receiving this notification because you are the one who triggered a build on the #{@project.name} project.
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index f08cb0a5428..3d28eec84ef 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -2,6 +2,7 @@
%html{ lang: "en"}
= render "layouts/head"
%body.ui_charcoal.login-page.application.navless
+ = Gon::Base.render_data
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 7c061dd531f..6bd427b02ac 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -2,6 +2,7 @@
%html{ lang: "en"}
= render "layouts/head"
%body.ui_charcoal.login-page.application.navless
+ = Gon::Base.render_data
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml
new file mode 100644
index 00000000000..c258eafdd51
--- /dev/null
+++ b/app/views/layouts/devise_mailer.html.haml
@@ -0,0 +1,34 @@
+!!! 5
+%html
+ %head
+ %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type')
+ = stylesheet_link_tag 'mailers/devise'
+
+ %body
+ %table#wrapper
+ %tr
+ %td
+ %table#header
+ %td{valign: "top"}
+ = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark')
+
+ %table#body
+ %tr
+ %td#body-container
+ = yield
+
+ - if Gitlab.com?
+ %table#footer
+ %tr
+ %td#tanuki
+ = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo')
+ %tr
+ %td#tagline
+ Everyone can contribute
+ %tr
+ %td#social
+ = link_to 'Blog', 'https://about.gitlab.com/blog/'
+ = link_to 'Twitter', 'https://twitter.com/gitlab'
+ = link_to 'Facebook', 'https://www.facebook.com/gitlab/'
+ = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg'
+ = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com'
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 915acc4612e..7fbe065df00 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -2,6 +2,7 @@
%html{ lang: "en"}
= render "layouts/head"
%body{class: "#{user_application_theme} application navless"}
+ = Gon::Base.render_data
= render "layouts/header/empty"
.container.navless-container
= render "layouts/flash"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 172579dafda..11cee421a99 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,7 +6,7 @@
= icon('bars')
%button.navbar-toggle{type: 'button'}
%span.sr-only Toggle navigation
- = icon('angle-left')
+ = icon('ellipsis-v')
.navbar-collapse.collapse
%ul.nav.navbar-nav
@@ -27,7 +27,7 @@
%li
= link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
- %span.badge.todos-pending-count
+ %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count
- if current_user.can_create_project?
%li
@@ -38,9 +38,19 @@
= 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')
+ %li.header-user.dropdown
+ = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
+ = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
+ %span.caret
+ .dropdown-menu-nav.dropdown-menu-align-right
+ %ul
+ %li
+ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
+ %li
+ = link_to "Profile Settings", profile_path
+ %li.divider
+ %li
+ = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", title: 'Sign out'
- else
%li
%div
@@ -49,6 +59,10 @@
%h1.title= title
+ .header-logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+
= yield :header_content
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 280a1b93729..5ee8772882e 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,102 +1,40 @@
-%ul.nav.nav-sidebar
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- = icon('dashboard fw')
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- = icon('cube fw')
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- = icon('user fw')
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- = icon('group fw')
- %span
- Groups
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- = icon('key fw')
- %span
- Deploy Keys
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- = icon('cog fw')
- %span
- Runners
- %span.count= number_with_delimiter(Ci::Runner.count(:all))
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- = icon('link fw')
- %span
- Builds
- %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')
- %span
- Logs
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
- = icon('bullhorn fw')
- %span
- Messages
- = nav_link(controller: :hooks) do
- = link_to admin_hooks_path, title: 'Hooks' do
- = icon('external-link fw')
- %span
- Hooks
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- = 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
- = icon('cloud fw')
- %span
- Applications
-
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- = icon('copy fw')
- %span
- Service Templates
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
- = icon('tags fw')
- %span
- Labels
+.scrolling-tabs-container{ class: nav_control_class }
+ = render 'layouts/nav/admin_settings'
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
+ %span
+ Overview
+ = nav_link(controller: %w(system_info background_jobs logs health_check)) do
+ = link_to admin_system_info_path, title: 'Monitoring' do
+ %span
+ Monitoring
+ = nav_link(controller: :broadcast_messages) do
+ = link_to admin_broadcast_messages_path, title: 'Messages' do
+ %span
+ Messages
+ = nav_link(controller: :hooks) do
+ = link_to admin_hooks_path, title: 'Hooks' do
+ %span
+ System Hooks
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse Reports" do
- = icon('exclamation-circle fw')
- %span
- Abuse Reports
- %span.count= number_with_delimiter(AbuseReport.count(:all))
+ = nav_link(controller: :applications) do
+ = link_to admin_applications_path, title: 'Applications' do
+ %span
+ Applications
- - if askimet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
- = icon('exclamation-triangle fw')
+ = nav_link(controller: :abuse_reports) do
+ = link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
- Spam Logs
- %span.count= number_with_delimiter(SpamLog.count(:all))
+ Abuse Reports
+ %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
- = link_to admin_application_settings_path, title: 'Settings' do
- = icon('cogs fw')
- %span
- Settings
+ - if askimet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = link_to admin_spam_logs_path, title: "Spam Logs" do
+ %span
+ Spam Logs
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
new file mode 100644
index 00000000000..38e9b80d129
--- /dev/null
+++ b/app/views/layouts/nav/_admin_settings.html.haml
@@ -0,0 +1,31 @@
+.controls
+ .dropdown.admin-settings-dropdown
+ %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(controller: :deploy_keys) do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
+ %span
+ Deploy Keys
+
+ = nav_link(controller: :services) do
+ = link_to admin_application_settings_services_path, title: 'Service Templates' do
+ %span
+ Service Templates
+
+ = nav_link(controller: :labels) do
+ = link_to admin_labels_path, title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ %span
+ Appearance
+
+ %li.divider
+ = nav_link(controller: :application_settings) do
+ = link_to admin_application_settings_path, title: 'Settings' do
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index fad4224e945..52e41b1a857 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,55 +1,64 @@
%ul.nav.nav-sidebar
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
- = link_to dashboard_projects_path, title: 'Projects' do
- = icon('bookmark fw')
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ .icon-container
+ = navbar_icon('project')
%span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
- = icon('bell fw')
+ .icon-container
+ = icon('bell fw')
%span
Todos
- %span.count.todos-pending-count= number_with_delimiter(todos_pending_count)
+ %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')
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ .icon-container
+ = navbar_icon('activity')
%span
Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
- = icon('group fw')
+ .icon-container
+ = navbar_icon('group')
%span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
- = icon('clock-o fw')
+ .icon-container
+ = navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do
- = icon('exclamation-circle fw')
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+ .icon-container
+ = navbar_icon('issues')
%span
Issues
%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')
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
+ .icon-container
+ = navbar_icon('mr')
%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')
+ .icon-container
+ = icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
- = icon('question-circle fw')
+ .icon-container
+ = icon('question-circle fw')
%span
Help
-
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
- = icon('user fw')
+ .icon-container
+ = icon('user fw')
%span
Profile Settings
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 3438005863a..d7d36c84b6c 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,37 +1,35 @@
-= render 'layouts/nav/group_settings'
-
-%ul.nav-links
- = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
- = link_to group_path(@group), title: 'Home' do
- = icon('group fw')
- %span
- Group
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- = icon('dashboard fw')
- %span
- Activity
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- = icon('clock-o fw')
- %span
- Milestones
- = nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues' do
- = icon('exclamation-circle fw')
- %span
- Issues
- - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- %span.badge.count= number_with_delimiter(issues.count)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- %span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members' do
- = icon('users fw')
- %span
- Members
+.scrolling-tabs-container{ class: nav_control_class }
+ = render 'layouts/nav/group_settings'
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
+ = link_to group_path(@group), title: 'Home' do
+ %span
+ Group
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ %span
+ Activity
+ = nav_link(controller: [:group, :milestones]) do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ %span
+ Milestones
+ = nav_link(path: 'groups#issues') do
+ = link_to issues_group_path(@group), title: 'Issues' do
+ %span
+ Issues
+ - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.badge.count= number_with_delimiter(issues.count)
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
+ %span
+ Merge Requests
+ - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.badge.count= number_with_delimiter(merge_requests.count)
+ = nav_link(controller: [:group_members]) do
+ = link_to group_group_members_path(@group), title: 'Members' do
+ %span
+ Members
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index e391ec7f2b7..bf9a7ecb786 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,20 +1,22 @@
- if current_user
- - if access = @group.users.find_by(id: current_user.id)
- .controls
- %span.dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can?(current_user, :admin_group, @group)
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- Projects
- %li.divider
- %li
- = link_to edit_group_path(@group) do
- Edit Group
+ - can_edit = can?(current_user, :admin_group, @group)
+ - member = @group.members.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_group_member, member)
+
+ .controls
+ .dropdown.group-settings-dropdown
+ %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(path: 'groups#projects') do
+ = link_to 'Projects', projects_group_path(@group), title: 'Projects'
+ %li.divider
+ - if can_edit
%li
- = link_to leave_group_group_members_path(@group),
- data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do
+ = link_to 'Edit Group', edit_group_path(@group)
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @group, :members]),
+ data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d730840d63a..96fe62c39c3 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,49 +1,49 @@
-%ul.nav-links
- = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile Settings' do
- = icon('user fw')
- %span
- Profile
- = nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account' do
- = icon('gear fw')
- %span
- Account
- = nav_link(controller: 'oauth/applications') do
- = link_to applications_profile_path, title: 'Applications' do
- = icon('cloud fw')
- %span
- Applications
- = nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails' do
- = icon('envelope-o fw')
- %span
- Emails
- - unless current_user.ldap_user?
- = nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password' do
- = icon('lock fw')
- %span
- Password
- = nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications' do
- = icon('inbox fw')
- %span
- Notifications
+.scrolling-tabs-container
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
+ = link_to profile_path, title: 'Profile Settings' do
+ %span
+ Profile
+ = nav_link(controller: [:accounts, :two_factor_auths]) do
+ = link_to profile_account_path, title: 'Account' do
+ %span
+ Account
+ - if current_application_settings.user_oauth_applications?
+ = nav_link(controller: 'oauth/applications') do
+ = link_to applications_profile_path, title: 'Applications' do
+ %span
+ Applications
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
+ %span
+ Personal Access Tokens
+ = nav_link(controller: :emails) do
+ = link_to profile_emails_path, title: 'Emails' do
+ %span
+ Emails
+ - unless current_user.ldap_user?
+ = nav_link(controller: :passwords) do
+ = link_to edit_profile_password_path, title: 'Password' do
+ %span
+ Password
+ = nav_link(controller: :notifications) do
+ = link_to profile_notifications_path, title: 'Notifications' do
+ %span
+ Notifications
- = nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys' do
- = icon('key fw')
- %span
- SSH Keys
- = nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences' do
- -# TODO (rspeicher): Better icon?
- = icon('image fw')
- %span
- Preferences
- = nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Audit Log' do
- = icon('history fw')
- %span
- Audit Log
+ = nav_link(controller: :keys) do
+ = link_to profile_keys_path, title: 'SSH Keys' do
+ %span
+ SSH Keys
+ = nav_link(controller: :preferences) do
+ = link_to profile_preferences_path, title: 'Preferences' do
+ %span
+ Preferences
+ = nav_link(path: 'profiles#audit_log') do
+ = link_to audit_log_profile_path, title: 'Audit Log' do
+ %span
+ Audit Log
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 479bde33719..9e65d94186b 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,131 +1,115 @@
-%ul.nav.nav-sidebar
- - if @project.group
- = nav_link do
- = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
- = icon('caret-square-o-left fw')
+- if current_user
+ .controls
+ .dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - can_edit = can?(current_user, :admin_project, @project)
+ -# We don't use @project.team.find_member because it searches for group members too...
+ - member = @project.members.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_project_member, member)
+
+ = render 'layouts/nav/project_settings', can_edit: can_edit
+
+ - if can_edit || can_leave
+ %li.divider
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @project, :members]),
+ data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
+
+.scrolling-tabs-container{ class: nav_control_class }
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
+ = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
%span
- Go to group
- - else
- = nav_link do
- = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Go to dashboard
-
- %li.separate-item
-
- = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
- = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('bookmark fw')
- %span
- Project
- = nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- = icon('dashboard fw')
- %span
- Activity
- - if project_nav_tab? :files
- = 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
- Files
+ Project
- - if project_nav_tab? :commits
- = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
- = icon('history fw')
+ = nav_link(path: 'projects#activity') do
+ = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
%span
- Commits
+ Activity
+
+ - if project_nav_tab? :files
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
+ = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
+ %span
+ Repository
+
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
+
+ - if project_nav_tab? :container_registry
+ = nav_link(controller: %w(container_registry)) do
+ = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
+ %span
+ Registry
+
+ - if project_nav_tab? :graphs
+ = nav_link(controller: %w(graphs)) do
+ = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
+ %span
+ Graphs
+
+ - if project_nav_tab? :issues
+ = nav_link(controller: [:issues, :labels, :milestones]) do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
+ %span
+ Issues
+ - if @project.default_issues_tracker?
+ %span.badge.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
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
+ %span
+ Merge Requests
+ %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
+
+ - if project_nav_tab? :wiki
+ = nav_link(controller: :wikis) do
+ = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
+ %span
+ Wiki
+
+ - 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
+ %span
+ Snippets
+
+ -# Global shortcut to network page for compatibility
+ - if project_nav_tab? :network
+ %li.hidden
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
+ Network
+
+ -# Shortcut to create a new issue
+ %li.hidden
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do
+ Create a new issue
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- = icon('cubes fw')
- %span
+ -# Shortcut to builds page
+ - if project_nav_tab? :builds
+ %li.hidden
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
Builds
- %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
- = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
- = icon('area-chart fw')
- %span
- Graphs
-
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- = icon('clock-o fw')
- %span
- Milestones
-
- - if project_nav_tab? :issues
- = nav_link(controller: :issues) do
- = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
- = icon('exclamation-circle fw')
- %span
- Issues
- - if @project.default_issues_tracker?
- %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
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
-
- - if project_nav_tab? :team
- = nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
- = icon('users fw')
- %span
- Members
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- = icon('tags fw')
- %span
- Labels
-
- - if project_nav_tab? :wiki
- = nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
- = icon('book fw')
- %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
- = icon('clipboard fw')
- %span
- Snippets
-
- - if project_nav_tab? :settings
- = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
- = link_to edit_project_path(@project), title: 'Settings' do
- = icon('cogs fw')
- %span
- Settings
-
- -# Global shortcut to network page for compatibility
- - if project_nav_tab? :network
- %li.hidden
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
- Network
-
- -# Shortcut to create a new issue
- %li.hidden
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do
- Create a new issue
+ -# Shortcut to commits page
+ - if project_nav_tab? :commits
+ %li.hidden
+ = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
+ Commits
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index d429a928464..51a54b4f262 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,63 +1,45 @@
-%ul.nav.nav-sidebar
- = nav_link do
- = link_to project_path(@project), title: 'Go to project', class: 'back-link' do
- = icon('caret-square-o-left fw')
+- if project_nav_tab? :team
+ = nav_link(controller: [:project_members, :teams]) do
+ = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
- Go to project
-
- %li.separate-item
-
- %ul.sidebar-subnav
- = nav_link(path: 'projects#edit') do
- = link_to edit_project_path(@project), title: 'Project Settings' do
- = icon('pencil-square-o fw')
+ Members
+- if can_edit
+ - 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
%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')
+ Groups
+ = nav_link(controller: :deploy_keys) do
+ = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
+ %span
+ Deploy Keys
+ = nav_link(controller: :hooks) do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
+ %span
+ Webhooks
+ = nav_link(controller: :services) do
+ = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
+ %span
+ Services
+ = nav_link(controller: :protected_branches) do
+ = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
+ %span
+ Protected Branches
+
+ - if @project.builds_enabled?
+ = nav_link(controller: :runners) do
+ = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span
- Deploy Keys
- = nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
- = icon('link fw')
+ Runners
+ = nav_link(controller: :variables) do
+ = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
%span
- Webhooks
- = nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
- = icon('cogs fw')
+ Variables
+ = nav_link(controller: :triggers) do
+ = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
%span
- Services
- = nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
- = icon('lock fw')
+ Triggers
+ = nav_link(controller: :badges) do
+ = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
%span
- Protected Branches
-
- - if @project.builds_enabled?
- = nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
- = icon('cog fw')
- %span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
- = icon('code fw')
- %span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
- = icon('retweet fw')
- %span
- Triggers
- = nav_link(controller: :badges) do
- = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
- = icon('star-half-empty fw')
- %span
- Badges
+ Badges
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 2997f59d946..dde2e2889dc 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -4,6 +4,7 @@
%title
GitLab
= stylesheet_link_tag 'notify'
+ = yield :head
%body
%div.content
= yield
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 6dfe7fbdae8..2049b204956 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,12 +1,12 @@
- 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
+- nav "project"
- content_for :scripts_body_top do
- project = @target_project || @project
- - if @project_wiki
- - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project)
+ - if @project_wiki && @page
+ - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id])
- else
- markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user
diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml
index 59ce38f67bb..4bc94bd132d 100644
--- a/app/views/layouts/project_settings.html.haml
+++ b/app/views/layouts/project_settings.html.haml
@@ -1,5 +1,4 @@
- page_title "Settings"
-- header_title project_title(@project, "Settings", edit_project_path(@project))
-- sidebar "project_settings"
+- nav "project"
= render template: "layouts/project"
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 12ded41fbf2..e9c66170877 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -2,4 +2,4 @@
%div
#{link_to @note.author_name, user_url(@note.author)} wrote:
%div
- = markdown(@note.note, pipeline: :email)
+ = markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index 81d65037312..4bf7c1f4d64 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -10,7 +10,7 @@
%p
Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
- Author: #{@build.commit.git_author_name}
+ Author: #{@build.pipeline.git_author_name}
%p
Branch: #{@build.ref}
%p
@@ -18,7 +18,7 @@
%p
Job: #{@build.name}
%p
- Message: #{@build.commit.git_commit_message}
+ Message: #{@build.pipeline.git_commit_message}
%p
Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
index 675acea60a1..9d497983498 100644
--- a/app/views/notify/build_fail_email.text.erb
+++ b/app/views/notify/build_fail_email.text.erb
@@ -1,11 +1,11 @@
Build failed for <%= @project.name %>
Status: <%= @build.status %>
-Commit: <%= @build.commit.short_sha %>
-Author: <%= @build.commit.git_author_name %>
+Commit: <%= @build.pipeline.short_sha %>
+Author: <%= @build.pipeline.git_author_name %>
Branch: <%= @build.ref %>
Stage: <%= @build.stage %>
Job: <%= @build.name %>
-Message: <%= @build.commit.git_commit_message %>
+Message: <%= @build.pipeline.git_commit_message %>
Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 5d247eb4cf2..252a5b7152c 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -10,7 +10,7 @@
%p
Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
- Author: #{@build.commit.git_author_name}
+ Author: #{@build.pipeline.git_author_name}
%p
Branch: #{@build.ref}
%p
@@ -18,7 +18,7 @@
%p
Job: #{@build.name}
%p
- Message: #{@build.commit.git_commit_message}
+ Message: #{@build.pipeline.git_commit_message}
%p
Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
index 747da44acae..c5ed4f84861 100644
--- a/app/views/notify/build_success_email.text.erb
+++ b/app/views/notify/build_success_email.text.erb
@@ -1,11 +1,11 @@
Build successful for <%= @project.name %>
Status: <%= @build.status %>
-Commit: <%= @build.commit.short_sha %>
-Author: <%= @build.commit.git_author_name %>
+Commit: <%= @build.pipeline.short_sha %>
+Author: <%= @build.pipeline.git_author_name %>
Branch: <%= @build.ref %>
Stage: <%= @build.stage %>
Job: <%= @build.name %>
-Message: <%= @build.commit.git_commit_message %>
+Message: <%= @build.pipeline.git_commit_message %>
Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml
deleted file mode 100644
index f1916d624b6..00000000000
--- a/app/views/notify/group_access_granted_email.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%p
- = "You have been granted #{@group_member.human_access} access to group"
- = link_to group_url(@group) do
- = @group.name
diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb
deleted file mode 100644
index ef9617bfc16..00000000000
--- a/app/views/notify/group_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
-
-<%= url_for(group_url(@group)) %>
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
deleted file mode 100644
index 55efad384a7..00000000000
--- a/app/views/notify/group_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
- #{@group_member.invite_email}, now known as
- #{link_to @group_member.user.name, user_url(@group_member.user)},
- has accepted your invitation to join group
- #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb
deleted file mode 100644
index f8b70f7a5a6..00000000000
--- a/app/views/notify/group_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml
deleted file mode 100644
index f9525d84fac..00000000000
--- a/app/views/notify/group_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- #{@invite_email}
- has declined your invitation to join group
- #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb
deleted file mode 100644
index 6c19a288d15..00000000000
--- a/app/views/notify/group_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml
deleted file mode 100644
index 163e88bfea3..00000000000
--- a/app/views/notify/group_member_invited_email.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%p
- You have been invited
- - if inviter = @group_member.created_by
- by
- = link_to inviter.name, user_url(inviter)
- to join group
- = link_to @group.name, group_url(@group)
- as #{@group_member.human_access}.
-
-%p
- = link_to 'Accept invitation', invite_url(@token)
- or
- = link_to 'decline', decline_invite_url(@token)
-
diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb
deleted file mode 100644
index 28ce4819b14..00000000000
--- a/app/views/notify/group_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml
new file mode 100644
index 00000000000..71c9c50071a
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.html.haml
@@ -0,0 +1,4 @@
+%p
+ Your request to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}
+ has been denied.
diff --git a/app/views/notify/member_access_denied_email.text.erb b/app/views/notify/member_access_denied_email.text.erb
new file mode 100644
index 00000000000..87f2ef817ee
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.text.erb
@@ -0,0 +1,3 @@
+Your request to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> has been denied.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
new file mode 100644
index 00000000000..18dec806539
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ You have been granted #{member.human_access} access to the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb
new file mode 100644
index 00000000000..a9fb3a589a5
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.text.erb
@@ -0,0 +1,3 @@
+You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_requested_email.html.haml b/app/views/notify/member_access_requested_email.html.haml
new file mode 100644
index 00000000000..76f1f08a0cb
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{link_to member.user.name, member.user} requested #{member.human_access}
+ access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
new file mode 100644
index 00000000000..9c5ee0eaf26
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.html.haml b/app/views/notify/member_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..2d1d40881eb
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{member.invite_email}, now known as
+ #{link_to member.user.name, user_url(member.user)},
+ has accepted your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..cef87101427
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invite_declined_email.html.haml b/app/views/notify/member_invite_declined_email.html.haml
new file mode 100644
index 00000000000..aa1b373d1a6
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.html.haml
@@ -0,0 +1,4 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_declined_email.text.erb b/app/views/notify/member_invite_declined_email.text.erb
new file mode 100644
index 00000000000..8bc305910c4
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
new file mode 100644
index 00000000000..b8b75da3f2f
--- /dev/null
+++ b/app/views/notify/member_invited_email.html.haml
@@ -0,0 +1,13 @@
+%p
+ You have been invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_url(member.created_by)
+ to join the
+ = link_to member_source.human_name, member_source.web_url
+ #{member_source.model_name.singular} as #{member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
new file mode 100644
index 00000000000..0a6393355be
--- /dev/null
+++ b/app/views/notify/member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index ad3ab2525bb..f42b150c0d6 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -2,7 +2,7 @@
%div
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
-if @issue.description
- = markdown(@issue.description, pipeline: :email)
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
%p
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 23423e7d981..158404de396 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -9,4 +9,4 @@
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
-if @merge_request.description
- = markdown(@merge_request.description, pipeline: :email)
+ = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml
index 65f0e4c4068..35c4b862bb7 100644
--- a/app/views/notify/note_merge_request_email.html.haml
+++ b/app/views/notify/note_merge_request_email.html.haml
@@ -1,7 +1,7 @@
-- if @note.diff_file_name
+- if @note.diff_note?
%p.details
New comment on diff for
- = link_to @note.diff_file_name, @target_url
+ = link_to @note.diff_file.file_path, @target_url
\:
= render 'note_message'
diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml
deleted file mode 100644
index dfc30a2d360..00000000000
--- a/app/views/notify/project_access_granted_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- = "You have been granted #{@project_member.human_access} access to project"
-%p
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name_with_namespace
diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb
deleted file mode 100644
index 68eb1611ba7..00000000000
--- a/app/views/notify/project_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>
-
-<%= url_for(namespace_project_url(@project.namespace, @project)) %>
diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml
deleted file mode 100644
index 7e58d30b10a..00000000000
--- a/app/views/notify/project_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
- #{@project_member.invite_email}, now known as
- #{link_to @project_member.user.name, user_url(@project_member.user)},
- has accepted your invitation to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb
deleted file mode 100644
index fcbe752114d..00000000000
--- a/app/views/notify/project_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml
deleted file mode 100644
index c2d7e6f6e3a..00000000000
--- a/app/views/notify/project_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- #{@invite_email}
- has declined your invitation to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb
deleted file mode 100644
index 484687fa51c..00000000000
--- a/app/views/notify/project_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml
deleted file mode 100644
index 79eb89616de..00000000000
--- a/app/views/notify/project_member_invited_email.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%p
- You have been invited
- - if inviter = @project_member.created_by
- by
- = link_to inviter.name, user_url(inviter)
- to join project
- = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
- as #{@project_member.human_access}.
-
-%p
- = link_to 'Accept invitation', invite_url(@token)
- or
- = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb
deleted file mode 100644
index e0706272115..00000000000
--- a/app/views/notify/project_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
new file mode 100644
index 00000000000..b28fea35ad5
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -0,0 +1,8 @@
+%p
+ Project #{@project.name} was exported successfully.
+%p
+ The project export can be downloaded from:
+ = link_to download_export_namespace_project_url(@project.namespace, @project) do
+ = @project.name_with_namespace + " export"
+%p
+ The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_exported_email.text.erb b/app/views/notify/project_was_exported_email.text.erb
new file mode 100644
index 00000000000..42c4d176876
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.text.erb
@@ -0,0 +1,6 @@
+Project <%= @project.name %> was exported successfully.
+
+The project export can be downloaded from:
+<%= download_export_namespace_project_url(@project.namespace, @project) %>
+
+The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml
new file mode 100644
index 00000000000..c888da29c17
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.html.haml
@@ -0,0 +1,9 @@
+%p
+ Project #{@project.name} couldn't be exported.
+%p
+ The errors we encountered were:
+
+ %ul
+ - @errors.each do |error|
+ %li
+ #{error}
diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml
new file mode 100644
index 00000000000..b27cb620b9e
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.text.haml
@@ -0,0 +1,6 @@
+= "Project #{@project.name} couldn't be exported."
+
+= "The errors we encountered were:"
+
+- @errors.each do |error|
+ #{error} \ No newline at end of file
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index f2e405b14fd..c161ecc3463 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,3 +1,6 @@
+= content_for :head do
+ = stylesheet_link_tag 'mailers/repository_push_email'
+
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))}
@@ -43,26 +46,37 @@
= diff.new_path
- unless @message.disable_diffs?
- %h4 Changes:
- - @message.diffs.each_with_index do |diff, i|
- %li{id: "diff-#{i}"}
- %a{href: @message.target_url + "#diff-#{i}"}
- - if diff.deleted_file
- %strong
- = diff.old_path
- deleted
- - elsif diff.renamed_file
- %strong
- = diff.old_path
- &rarr;
- %strong
- = diff.new_path
- - else
- %strong
- = diff.new_path
- %hr
- = color_email_diff(diff.diff)
- %br
+ - diff_files = @message.diffs
- - if @message.compare_timeout
- %h5 Huge diff. To prevent performance issues changes are hidden
+ - if @message.compare_timeout
+ %h5 The diff was not included because it is too large.
+ - else
+ %h4 Changes:
+ - diff_files.each_with_index do |diff_file, i|
+ %li{id: "diff-#{i}"}
+ %a{href: @message.target_url + "#diff-#{i}"}<
+ - if diff_file.deleted_file
+ %strong<
+ = diff_file.old_path
+ deleted
+ - elsif diff_file.renamed_file
+ %strong<
+ = diff_file.old_path
+ &rarr;
+ %strong<
+ = diff_file.new_path
+ - else
+ %strong<
+ = diff_file.new_path
+ - if diff_file.too_large?
+ The diff for this file was not included because it is too large.
+ - else
+ %hr
+ - blob = diff_file.blob
+ - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
+ %table.code.white
+ - diff_file.highlighted_diff_lines.each do |line|
+ = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
+ - else
+ No preview for this file type
+ %br
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 53869e36b28..5ac23aa3997 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -25,24 +25,28 @@
- else
\- #{diff.new_path}
- unless @message.disable_diffs?
- \
- \
- Changes:
- - @message.diffs.each do |diff|
+ - if @message.compare_timeout
\
- \=====================================
- - if diff.deleted_file
- #{diff.old_path} deleted
- - elsif diff.renamed_file
- #{diff.old_path} → #{diff.new_path}
- - else
- = diff.new_path
- \=====================================
- != diff.diff
- - if @message.compare_timeout
- \
- \
- Huge diff. To prevent performance issues it was hidden
+ \
+ The diff was not included because it is too large.
+ - else
+ \
+ \
+ Changes:
+ - @message.diffs.each do |diff_file|
+ \
+ \=====================================
+ - if diff_file.deleted_file
+ #{diff_file.old_path} deleted
+ - elsif diff_file.renamed_file
+ #{diff_file.old_path} → #{diff_file.new_path}
+ - else
+ = diff_file.new_path
+ \=====================================
+ - if diff_file.too_large?
+ The diff for this file was not included because it is too large.
+ - else
+ != diff_file.diff.diff
- if @message.target_url
\
\
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index afd3d79321f..8efe486e01b 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -11,7 +11,7 @@
%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|
+ = 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"
@@ -29,21 +29,22 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- Two-factor Authentication
+ Two-Factor Authentication
%p
- Increase your account's security by enabling two-factor authentication (2FA).
+ 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'
+ Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
+ - if current_user.two_factor_enabled?
+ = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info'
+ = link_to 'Disable', profile_two_factor_auth_path,
+ method: :delete,
+ data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
+ class: 'btn btn-danger'
- else
- = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
- data: { confirm: 'Are you sure?' }
+ .append-bottom-10
+ = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success'
+
%hr
- if button_based_providers.any?
.row.prepend-top-default
@@ -61,16 +62,20 @@
.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
+ - if provider.to_s == 'saml'
+ %a.provider-btn
+ Active
+ - else
+ = 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
+ = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "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
+ %h4.prepend-top-0.warning-title
Change username
%p
Changing your username will change path to all personal projects!
@@ -94,7 +99,7 @@
- if signup_enabled?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0.remove-account-title
+ %h4.prepend-top-0.danger-title
Remove account
.col-lg-9
- if @user.can_be_removed?
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 89ae7ffda2b..537bba21f4a 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -1,7 +1,7 @@
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
- = notification_icon(current_user.notification_level)
+ = notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
@@ -9,5 +9,4 @@
= link_to group.name, group_path(group)
.pull-right
- = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 17c097154da..5b2a69b8891 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -1,7 +1,7 @@
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
- = notification_icon(current_user.notification_level)
+ = notification_icon(current_user.global_notification_setting.level)
- else
= notification_icon(setting.level)
@@ -9,5 +9,4 @@
= link_to_project(project)
.pull-right
- = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 7696f112bb3..f77738f97f5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -24,38 +24,15 @@
.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: :disabled do
- = f.radio_button :notification_level, :disabled
- .level-title
- Disabled
- %p You will not get any notifications via email
-
- .radio
- = f.label :notification_level, value: :mention do
- = f.radio_button :notification_level, :mention
- .level-title
- On Mention
- %p You will receive notifications only for comments in which you were @mentioned
- .radio
- = f.label :notification_level, value: :participating do
- = f.radio_button :notification_level, :participating
- .level-title
- Participating
- %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
+ = label_tag :global_notification_level, "Global notification level", class: "label-light"
+ %br
+ .clearfix
+ .form-group.pull-left.global-notification-setting
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
- .radio
- = f.label :notification_level, value: :watch do
- = f.radio_button :notification_level, :watch
- .level-title
- Watch
- %p You will receive notifications for any activity
+ .clearfix
- .prepend-top-default
- = f.submit 'Update settings', class: "btn btn-create"
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..1b45548bd02
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -0,0 +1,105 @@
+- page_title "Personal Access Tokens"
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can generate a personal access token for each application you use that needs access to the GitLab API.
+ .col-lg-9
+
+ - if flash[:personal_access_token]
+ .created-personal-access-token-container
+ %h5.prepend-top-0
+ Your New Personal Access Token
+ .form-group
+ = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
+ = clipboard_button(clipboard_text: flash[:personal_access_token])
+ %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
+
+ %hr
+
+ %h5.prepend-top-0
+ Add a Personal Access Token
+ %p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique token.
+ = form_for [:profile, @personal_access_token],
+ method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(@personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control", required: false
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
+
+ %hr
+
+ %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
+
+ - if @active_personal_access_tokens.present?
+ .table-responsive
+ %table.table.active-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th
+ %tbody
+ - @active_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires_at.present?
+ = token.expires_at.to_date.to_s(:medium)
+ - else
+ %span.personal-access-tokens-never-expires-label Never
+ %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any active tokens yet.
+
+ %hr
+
+ %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
+
+ - if @inactive_personal_access_tokens.present?
+ .table-responsive
+ %table.table.inactive-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %tbody
+ - @inactive_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+
+ - else
+ .settings-message.text-center
+ There are no inactive tokens.
+
+
+:javascript
+ var date = $('#personal_access_token_expires_at').val();
+
+ var datepicker = $(".datepicker").datepicker({
+ dateFormat: "yy-mm-dd",
+ minDate: 0
+ });
+
+ $("#created-personal-access-token").click(function() {
+ this.select();
+ });
+
+ $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index bfe53be6854..1b1b16d656f 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -5,7 +5,7 @@
%h4.prepend-top-0
Application theme
%p
- This setting allows you to customize the appearance of the site, ex. sidebar.
+ This setting allows you to customize the appearance of the site, e.g. the sidebar.
.col-lg-9.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
deleted file mode 100644
index 69fc81cb45c..00000000000
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-- page_title 'Two-factor Authentication', 'Account'
-
-.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
- 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/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
new file mode 100644
index 00000000000..593be2617c1
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -0,0 +1,69 @@
+- page_title 'Two-Factor Authentication', 'Account'
+- header_title "Two-Factor Authentication", profile_two_factor_auth_path
+
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Register Two-Factor Authentication App
+ %p
+ Use an app on your mobile device to enable two-factor authentication (2FA).
+ .col-lg-9
+ - if current_user.two_factor_otp_enabled?
+ = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
+ - else
+ %p
+ Download the Google Authenticator application from App Store or Google Play Store 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 'Register with Two-Factor App', class: 'btn btn-success'
+
+%hr
+
+.row.prepend-top-default
+
+ .col-lg-3
+ %h4.prepend-top-0
+ Register Universal Two-Factor (U2F) Device
+ %p
+ Use a hardware device to add the second factor of authentication.
+ %p
+ As U2F devices are only supported by a few browsers, we require that you set up a
+ two-factor authentication app before a U2F device. That way you'll always be able to
+ log in - even when you're using an unsupported browser.
+ .col-lg-9
+ %p
+ - if @registration_key_handles.present?
+ = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
+ - if @u2f_registration.errors.present?
+ = form_errors(@u2f_registration)
+ = render "u2f/register"
+
+- if two_factor_skippable?
+ :javascript
+ var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
+ $(".flash-alert").append(button);
+
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
index 0de019983ca..0568c2d305e 100644
--- a/app/views/projects/_builds_settings.html.haml
+++ b/app/views/projects/_builds_settings.html.haml
@@ -1,74 +1,65 @@
%fieldset.builds-feature
- %legend
- Builds:
-
+ %h5.prepend-top-0
+ Builds
- unless @repository.gitlab_ci_yml
.form-group
- .col-sm-offset-2.col-sm-10
- %p Builds need to be configured before you can begin using Continuous Integration.
- = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
- %hr
-
+ %p Builds need to be configured before you can begin using Continuous Integration.
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
.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
+ %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
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
+ = 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+\%
- %li
- gcovr (C/C++) -
- %code ^TOTAL.*\s+(\d+\%)$
- %li
- tap --coverage-report=text-summary (Node.js) -
- %code ^Statements\s*:\s*([^%]+)
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ .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+\%
+ %li
+ gcovr (C/C++) -
+ %code ^TOTAL.*\s+(\d+\%)$
+ %li
+ tap --coverage-report=text-summary (Node.js) -
+ %code ^Statements\s*:\s*([^%]+)
.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
+ .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.
+ .form-group.append-bottom-0
+ = f.label :runners_token, "Runners token", class: 'label-light'
+ = 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/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml
deleted file mode 100644
index 46ad1559356..00000000000
--- a/app/views/projects/_github_import_modal.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%div#github_import_modal.modal
- .modal-dialog
- .modal-content
- .modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 Import projects from GitHub
- .modal-body
- To enable importing projects from GitHub,
- - if current_user.admin?
- as administrator you need to configure
- - else
- ask your Gitlab administrator to configure
- == #{link_to 'OAuth integration', help_page_path("integration", "github")}.
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 9b5de17dd3b..cf11723dc8e 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,60 +1,29 @@
- empty_repo = @project.empty_repo?
-.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)}
- .project-identicon-holder
- = project_icon(@project, alt: '', class: 'project-avatar avatar s90')
- .cover-title.project-home-desc
- %h1
+.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
+ %div{ class: container_class }
+ = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70')
+ %h1.project-title
= @project.name
%span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
= visibility_level_icon(@project.visibility_level, fw: false)
- - if @project.description.present?
- .cover-desc.project-home-desc
- = markdown(@project.description, pipeline: :description)
+ .project-home-desc
+ - if @project.description.present?
+ = markdown(@project.description, pipeline: :description)
- - if forked_from_project = @project.forked_from_project
- .cover-desc
- Forked from
- = link_to project_path(forked_from_project) do
- = forked_from_project.namespace.try(:name)
+ - if forked_from_project = @project.forked_from_project
+ %p
+ Forked from
+ = link_to project_path(forked_from_project) do
+ = forked_from_project.namespace.try(:name)
- .cover-controls
- - if current_user
- = 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.project-action-buttons
+ .count-buttons
+ = render 'projects/buttons/star'
+ = render 'projects/buttons/fork'
- .project-repo-buttons
- .split-one.count-buttons
- = render 'projects/buttons/star'
- = render 'projects/buttons/fork'
-
- .clone-row
.project-clone-holder
= render "shared/clone_panel"
- .split-repo-buttons
- .btn-group.pull-left
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
-
- = render 'projects/buttons/notifications'
-
:javascript
new Star();
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 66c30283c7a..630ae7d6140 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,11 +1,10 @@
-.project-last-commit
- - if commit.status
- = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do
- = ci_icon_for_status(commit.status)
- = ci_label_for_status(commit.status)
+- if commit.status
+ = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do
+ = ci_icon_for_status(commit.status)
+ = ci_label_for_status(commit.status)
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
- &middot;
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
- = commit_author_link(commit, avatar: true, size: 24)
+= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
+&middot;
+#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 7c2b8d01508..3c6b931f41a 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,15 +1,17 @@
- if event = last_push_event
- if show_last_push_widget?(event)
-
.row-content-block.top-block.clear-block.hidden-xs
- .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
- %strong= event.ref_name
- branch
- #{time_ago_with_tooltip(event.created_at)}
+ %div{ class: container_class }
+ .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
+ %strong= event.ref_name
+ - if @project && event.project != @project
+ %span at
+ %strong= link_to_project event.project
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 8de44a6c914..58d961d93ca 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -7,9 +7,24 @@
%li
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
+
+ - if defined?(@issue) && @issue.confidential?
+ %li.confidential-issue-warning
+ = icon('warning')
+ %span This is a confidential issue. Your comment will not be visible to the public.
+
%li.pull-right
- %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
- Go full screen
+ .toolbar-group
+ = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ .toolbar-group
+ %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ =icon("arrows-alt fw")
.md-write-holder
= yield
@@ -18,7 +33,7 @@
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
%span
- = icon('exclamation-triangle')
+ = icon("exclamation-triangle")
You are about to add
%strong
%span.js-referenced-users-count 0
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
new file mode 100644
index 00000000000..771a2e0df7d
--- /dev/null
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -0,0 +1,11 @@
+%fieldset.builds-feature
+ %h5.prepend-top-0
+ Merge Requests
+ .form-group
+ .checkbox
+ = f.label :only_allow_merge_if_build_succeeds do
+ = f.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ .help-block
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index e1e35013968..413477a2d3a 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -4,5 +4,5 @@
= f.text_area attr, class: classes, placeholder: placeholder
- else
= text_area_tag attr, nil, class: classes, placeholder: placeholder
- %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
+ %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 69fa4ad37c4..3c0f01cbf6f 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,5 +1,4 @@
- page_title "Activity"
-- header_title project_title(@project, "Activity", activity_project_path(@project))
= render 'projects/last_push'
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 49f95ff37db..539d07d634a 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,5 +1,5 @@
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
-= render 'projects/builds/header_title'
+- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
index c22384ddf46..ac80951dd4f 100644
--- a/app/views/projects/badges/index.html.haml
+++ b/app/views/projects/badges/index.html.haml
@@ -1,6 +1,5 @@
- page_title 'Badges'
- badges_path = namespace_project_badges_path(@project.namespace, @project)
-- header_title project_title(@project, 'Badges', badges_path)
.prepend-top-10
.panel.panel-default
@@ -8,7 +7,7 @@
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
- = render 'shared/ref_switcher', destination: 'badges'
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 5f9a92ff93f..377665b096f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,5 +1,4 @@
- page_title "Blame", @blob.path, @ref
-- header_title project_title(@project, "Files", project_files_path(@project))
%h3.page-title Blame view
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index fefa652a3da..29c7d45074a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,9 +13,12 @@
required: true, class: 'form-control new-file-name'
.pull-right
- .license-selector.js-license-selector.hide
- = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
-
+ .license-selector.js-license-selector-wrap.hidden
+ = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+ .gitignore-selector.js-gitignore-selector-wrap.hidden
+ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
+ .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
+ = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/blob/_header_title.html.haml b/app/views/projects/blob/_header_title.html.haml
deleted file mode 100644
index 78c5ef20a5f..00000000000
--- a/app/views/projects/blob/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Files", project_files_path(@project))
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index b1769759dce..58524418a67 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -16,4 +16,4 @@
.file-content.code
.nothing-here-block Empty file
- else
- = render 'shared/file_highlight', blob: blob
+ = render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index effcce5a1c4..b1c9895f43e 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,16 +1,13 @@
- page_title "Edit", @blob.path, @ref
-= render "header_title"
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
- = icon('edit')
Edit File
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
- = icon('eye')
= editing_preview_title(@blob.name)
= 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
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 0459699432e..c952bc7e5db 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New File", @path.presence, @ref
-= render "header_title"
%h3.page-title
New File
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 6988039b6c7..0ab78a39cf9 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,13 +1,15 @@
+- @no_container = true
- page_title @blob.path, @ref
-= render "header_title"
+= render "projects/commits/head"
-= render 'projects/last_push'
+%div{ class: container_class }
+ = render 'projects/last_push'
-%div#tree-holder.tree-holder
- = render 'blob', blob: @blob
+ %div#tree-holder.tree-holder
+ = render 'blob', blob: @blob
-- if can_edit_blob?(@blob)
- = render 'projects/blob/remove'
+ - if can_edit_blob?(@blob)
+ = render 'projects/blob/remove'
- - title = "Replace #{@blob.name}"
- = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put
+ - title = "Replace #{@blob.name}"
+ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 57e507e68c8..4bd85061240 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -20,17 +20,15 @@
protected
.controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name)
- = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
- = icon('plus')
+ = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
- if branch.name != @repository.root_ref
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
- = icon("exchange")
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml
deleted file mode 100644
index a21ddaf4930..00000000000
--- a/app/views/projects/branches/destroy.js.haml
+++ /dev/null
@@ -1 +0,0 @@
-$('.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 ac7790421a4..77b405f1f39 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,33 +1,34 @@
+- @no_container = true
- page_title "Branches"
-= render "projects/commits/header_title"
= render "projects/commits/head"
-.row-content-block
- .pull-right
+
+%div{ class: container_class }
+ .top-area
+ .nav-text
+ Protected branches can be managed in project settings
+
- if can? current_user, :push_code, @project
- = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
- = icon('plus')
- New branch
- &nbsp;
- .dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = @sort.humanize
- - else
- Name
- %b.caret
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to namespace_project_branches_path(sort: nil) do
- Name
- = link_to namespace_project_branches_path(sort: 'recently_updated') do
- = sort_title_recently_updated
- = link_to namespace_project_branches_path(sort: 'last_updated') do
- = sort_title_oldest_updated
- .oneline
- Protected branches can be managed in project settings
-- unless @branches.empty?
- %ul.content-list.all-branches
- - @branches.each do |branch|
- = render "projects/branches/branch", branch: branch
- = paginate @branches, theme: 'gitlab'
+ .nav-controls
+ = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
+ New branch
+ .dropdown.inline
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = @sort.humanize
+ - else
+ Name
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ = link_to namespace_project_branches_path(sort: nil) do
+ Name
+ = link_to namespace_project_branches_path(sort: 'recently_updated') do
+ = sort_title_recently_updated
+ = link_to namespace_project_branches_path(sort: 'last_updated') do
+ = sort_title_oldest_updated
+ - if @branches.any?
+ %ul.content-list.all-branches
+ - @branches.each do |branch|
+ = render "projects/branches/branch", branch: branch
+ = paginate @branches, theme: 'gitlab'
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index c659af6338c..5a6c8c243fa 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Branch"
-= render "projects/commits/header_title"
- if @error
.alert.alert-danger
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
new file mode 100644
index 00000000000..51b5bd9db42
--- /dev/null
+++ b/app/views/projects/builds/_header.html.haml
@@ -0,0 +1,16 @@
+.content-block.build-header
+ = ci_status_with_icon(@build.status)
+ Build
+ %strong ##{@build.id}
+ for commit
+ = link_to ci_status_path(@build.pipeline) do
+ %strong= @build.pipeline.short_sha
+ from
+ = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
+ %code
+ = @build.ref
+ - if @build.user
+ = render "user"
+ = time_ago_with_tooltip(@build.created_at)
+ %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml
deleted file mode 100644
index 082dab1f5b0..00000000000
--- a/app/views/projects/builds/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Builds", project_builds_path(@project))
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
new file mode 100644
index 00000000000..cab21f0cf19
--- /dev/null
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -0,0 +1,107 @@
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+ .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
+ Build
+ %strong ##{@build.id}
+ %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
+ = icon('angle-double-right')
+ - if @build.coverage
+ .block.block-first
+ .title
+ Test coverage
+ %p.build-detail-row
+ #{@build.coverage}%
+
+ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+ .block{ class: ("block-first" if !@build.coverage) }
+ .title
+ Build artifacts
+ - if @build.artifacts_expired?
+ %p.build-detail-row
+ The artifacts were removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+ - elsif @build.artifacts_expire_at
+ %p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove= @build.artifacts_expire_at
+
+ - if @build.artifacts?
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.artifacts_expire_at
+ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+ Keep
+
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Download
+
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Browse
+
+ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
+ .title
+ Build details
+ - if @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ - if @build.merge_request
+ %p.build-detail-row
+ %span.build-light-text Merge Request:
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
+ - if @build.duration
+ %p.build-detail-row
+ %span.build-light-text Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ - if @build.finished_at
+ %p.build-detail-row
+ %span.build-light-text Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p.build-detail-row
+ %span.build-light-text Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
+ %p.build-detail-row
+ %span.build-light-text Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+ - if can?(current_user, :update_build, @project) && @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: "btn btn-sm btn-default", method: :post,
+ data: { confirm: "Are you sure you want to erase this build?" } do
+ Erase
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
+
+ %p
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.build-light-text Variables:
+
+ %code
+ - @build.trigger_request.variables.each do |key, value|
+ #{key}=#{value}
+
+ .block
+ .title
+ Commit message
+ %p.build-light-text.append-bottom-0
+ #{@build.pipeline.git_commit_message}
+
+ - if @build.tags.any?
+ .block
+ .title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml
new file mode 100644
index 00000000000..2642de8021d
--- /dev/null
+++ b/app/views/projects/builds/_user.html.haml
@@ -0,0 +1,4 @@
+by
+%a{ href: user_path(@build.user) }
+ = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
+ %strong= @build.user.to_reference
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 2e8015d119b..a131289ee97 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,65 +1,63 @@
+- @no_container = true
- page_title "Builds"
-= render "header_title"
-
-.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))
-
- %li{class: ('active' if @scope == 'finished')}
- = link_to project_builds_path(@project, scope: :finished) do
- Finished
- %span.badge.js-running-count
- = number_with_delimiter(@all_builds.finished.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
-
- - unless @repository.gitlab_ci_yml
- = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
-
- = link_to ci_lint_path, class: 'btn btn-default' do
- = icon('wrench')
- %span CI Lint
-
-.row-content-block
- #{(@scope || 'running').capitalize} builds from this project
-
-%ul.content-list
- - if @builds.blank?
- %li
- .nothing-here-block No builds to show
- - else
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Commit
- %th Ref
- %th Stage
- %th Name
- %th Tags
- %th Duration
- %th Finished at
- - if @project.build_coverage_enabled?
- %th Coverage
- %th
-
- = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
-
- = paginate @builds, theme: 'gitlab'
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ .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))
+
+ %li{class: ('active' if @scope == 'finished')}
+ = link_to project_builds_path(@project, scope: :finished) do
+ Finished
+ %span.badge.js-running-count
+ = number_with_delimiter(@all_builds.finished.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
+
+ - unless @repository.gitlab_ci_yml
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
+ = link_to ci_lint_path, class: 'btn btn-default' do
+ %span CI Lint
+
+ %ul.content-list
+ - if @builds.blank?
+ %li
+ .nothing-here-block No builds to show
+ - else
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Commit
+ %th Ref
+ %th Stage
+ %th Name
+ %th Tags
+ %th Duration
+ %th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
+ %th
+
+ = render @builds, commit_sha: true, ref: 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 c0f7a7686f0..4e801cc72fe 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,19 +1,11 @@
- page_title "#{@build.name} (##{@build.id})", "Builds"
-= render "header_title"
+- trace_with_state = @build.trace_with_state
+- header_title project_title(@project, "Builds", project_builds_path(@project))
.build-page
- .row-content-block.top-block
- Build ##{@build.id} for commit
- %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit)
- from
- = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- - merge_request = @build.merge_request
- - if merge_request
- via
- = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request)
+ = render "header"
- #up-build-trace
- - builds = @build.commit.builds.latest.to_a
+ - builds = @build.pipeline.builds.latest.to_a
- if builds.size > 1
%ul.nav-links.no-top.no-bottom
- builds.each do |build|
@@ -33,18 +25,6 @@
&middot;
%i.fa.fa-warning
This build was retried.
-
- .row-content-block.middle-block
- .build-head
- .clearfix
- = ci_status_with_icon(@build.status)
- - if @build.duration
- %span
- %i.fa.fa-time
- #{duration_in_words(@build.finished_at, @build.started_at)}
- .pull-right
- #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
-
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
@@ -64,156 +44,27 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
- .row.prepend-top-default
- .col-md-9
- .clearfix
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- .clearfix
+ .prepend-top-default
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ - 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
#js-build-scroll.scroll-controls
- = link_to '#up-build-trace', class: 'btn' do
+ = link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
+ %pre.build-trace#build-trace
+ %code.bash.js-build-output
+ = icon("refresh spin", class: "js-build-refresh")
- - 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
- - if @build.coverage
- .build-widget
- %h4.title
- Test coverage
- %h1 #{@build.coverage}%
-
- - 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.build-controls
- %h4.title
- Build ##{@build.id}
- - 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
- - if @build.has_trace?
- = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build),
- class: 'btn btn-sm btn-success', target: '_blank'
-
- .clearfix
- - if @build.duration
- %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
- %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 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
- %h4.title
- Trigger
-
- %p
- %span.attr-name Token:
- #{@build.trigger_request.trigger.short_token}
-
- - if @build.trigger_request.variables
- %p
- %span.attr-name Variables:
-
- %code
- - @build.trigger_request.variables.each do |key, value|
- #{key}=#{value}
-
- .build-widget
- %h4.title
- Commit
- .pull-right
- %small
- = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
- %p
- %span.attr-name Branch:
- = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- %p
- %span.attr-name Author:
- #{@build.commit.git_author_name}
- %p
- %span.attr-name Message:
- #{@build.commit.git_commit_message}
-
- - if @build.tags.any?
- .build-widget
- %h4.title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
-
- - if @builds.present?
- .build-widget
- %h4.title #{pluralize(@builds.count(:id), "other build")} for
- = succeed ":" do
- = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
- %table.table.builds
- - @builds.each_with_index do |build, i|
- %tr.build
- %td
- = ci_icon_for_status(build.status)
- %td
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- - if build.name
- = build.name
- - else
- %span ##{build.id}
-
- %td.status= build.status
+ #down-build-trace
+= render "sidebar"
- :javascript
- new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}")
+:javascript
+ new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 1e4c46fca2f..16b8e1cca91 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -2,7 +2,7 @@
.btn-group
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
- %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
+ %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- can_create_snippet = can?(current_user, :create_snippet, @project)
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 5fb5fe5af2f..a9eaed4c5f6 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -12,7 +12,7 @@
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
- = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do
+ %div.count-with-arrow
%span.arrow
- %span.count
+ = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
= @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
deleted file mode 100644
index c1e3e5b73a2..00000000000
--- a/app/views/projects/buttons/_notifications.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- if @notification_setting
- = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
- = f.hidden_field :level
- %span.dropdown
- %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
- = icon('bell')
- = notification_title(@notification_setting.level)
- = icon('angle-down')
- %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- - NotificationSetting.levels.each do |level|
- = notification_list_item(level.first, @notification_setting)
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 02dbb2985a4..71cf5582a4c 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: "Star project" do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
- if current_user.starred?(@project)
= icon('star fw')
%span.starred Unstar
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 8e95f040273..5bd6e3f0ebc 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -13,7 +13,9 @@
%strong ##{build.id}
- if build.stuck?
- %i.fa.fa-warning.text-warning
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ - if defined?(retried) && retried
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- if defined?(commit_sha) && commit_sha
%td
@@ -70,11 +72,11 @@
.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', class: 'btn btn-build' do
- %i.fa.fa-download
+ = icon('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', class: 'btn btn-build' do
- %i.fa.fa-remove.cred
+ = icon('remove', class: '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', class: 'btn btn-build' do
- %i.fa.fa-refresh
+ = icon('refresh')
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
new file mode 100644
index 00000000000..af8dd5cd02c
--- /dev/null
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -0,0 +1,71 @@
+- status = pipeline.status
+%tr.commit
+ %td.commit-link
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do
+ = ci_icon_for_status(status)
+ %strong ##{pipeline.id}
+
+ %td
+ %div.branch-commit
+ - if pipeline.ref
+ = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace"
+ &middot;
+ = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
+ &nbsp;
+ - if pipeline.tag?
+ %span.label.label-primary tag
+ - elsif pipeline.latest?
+ %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
+ - if pipeline.triggered?
+ %span.label.label-primary triggered
+ - if pipeline.yaml_errors.present?
+ %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
+ - if pipeline.builds.any?(&:stuck?)
+ %span.label.label-warning stuck
+
+ %p.commit-title
+ - if commit = pipeline.commit
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
+ - else
+ Cant find HEAD commit for this branch
+
+
+ - stages_status = pipeline.statuses.stages_status
+ - stages.each do |stage|
+ %td
+ - status = stages_status[stage]
+ - tooltip = "#{stage.titleize}: #{status || 'not found'}"
+ - if status
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = ci_icon_for_status(status)
+ - else
+ .light.has-tooltip{ title: tooltip }
+ \-
+
+ %td
+ - if pipeline.started_at && pipeline.finished_at
+ %p.duration
+ = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+
+ %td
+ .controls.hidden-xs.pull-right
+ - artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
+ - if artifacts.present?
+ .dropdown.inline.build-artifacts
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ = icon('download')
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - artifacts.each do |build|
+ %li
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
+ = icon("download")
+ %span Download '#{build.name}' artifacts
+
+ - if can?(current_user, :update_pipeline, @project)
+ - if pipeline.retryable?
+ = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = icon("repeat")
+ - if pipeline.cancelable?
+ = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = icon("remove")
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index 5c9a319edeb..a508382578a 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,2 +1,2 @@
-- @ci_commits.each do |ci_commit|
- = render "ci_commit", ci_commit: ci_commit
+- @pipelines.each do |pipeline|
+ = render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 44ef1fdbbe3..d9b800a4ded 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,7 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
- = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
+ = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml
deleted file mode 100644
index e849aefb188..00000000000
--- a/app/views/projects/commit/_ci_commit.html.haml
+++ /dev/null
@@ -1,71 +0,0 @@
-.row-content-block.build-content.middle-block
- .pull-right
- - if can?(current_user, :update_build, @project)
- - if ci_commit.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
-
- - if ci_commit.builds.running_or_pending.any?
- = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
-
- .oneline
- = pluralize ci_commit.statuses.count(:id), "build"
- - if ci_commit.ref
- for
- %span.label.label-info
- = ci_commit.ref
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
- - if ci_commit.duration
- in
- = time_interval_in_words ci_commit.duration
-
-- if ci_commit.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - ci_commit.yaml_errors.split(",").each do |error|
- %li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
-
-- if @project.builds_enabled? && !ci_commit.ci_yaml_file
- .bs-callout.bs-callout-warning
- \.gitlab-ci.yml not found in this commit
-
-.table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Stage
- %th Name
- %th Tags
- %th Duration
- %th Finished at
- - if @project.build_coverage_enabled?
- %th Coverage
- %th
- - builds = ci_commit.statuses.latest.ordered
- = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true
-
-- if ci_commit.retried.any?
- .row-content-block.second-block
- Retried builds
-
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Ref
- %th Stage
- %th Name
- %th Tags
- %th Duration
- %th Finished at
- - if @project.build_coverage_enabled?
- %th Coverage
- %th
- = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
new file mode 100644
index 00000000000..9d925cacc0d
--- /dev/null
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -0,0 +1,15 @@
+%tr
+ %th{colspan: 10}
+ %strong
+ %a{name: stage}
+ - status = statuses.latest.status
+ %span{class: "ci-status-link ci-status-icon-#{status}"}
+ = ci_icon_for_status(status)
+ - if stage
+ &nbsp;
+ = stage.titleize
+ = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
+ = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
+ %tr
+ %td{colspan: 10}
+ &nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 01163e526b2..3ad866bb2f1 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,37 +1,35 @@
-.pull-right.commit-action-buttons
- %div
- - if @notes_count > 0
- %span.btn.disabled.btn-grouped
- %i.fa.fa-comment
+.commit-info-row.commit-info-row-header
+ %span.hidden-xs Authored by
+ %strong
+ = commit_author_link(@commit, avatar: true, size: 24)
+ #{time_ago_with_tooltip(@commit.authored_date)}
+
+ .pull-right.commit-action-buttons
+ - if defined?(@notes_count) && @notes_count > 0
+ %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
+ = icon('comment')
= @notes_count
- .pull-left.btn-group
- %a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} }
- %i.fa.fa-download
- Download as
- %span.caret
- %ul.dropdown-menu
+ = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do
+ Browse Files
+ .dropdown.inline
+ %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
+ %span.hidden-xs Options
+ %span.caret.commit-options-dropdown-caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li.visible-xs-block.visible-sm-block
+ = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
+ Browse Files
+ - unless @commit.has_been_reverted?(current_user)
+ %li.clearfix
+ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
+ %li.clearfix
+ = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
+ %li.divider
+ %li.dropdown-header
+ Download
- unless @commit.parents.length > 1
%li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
%li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff)
- = 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))
- = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
- %div
-
-%p
-.commit-info-row
- - if @commit.status
- = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do
- = ci_icon_for_status(@commit.status)
- build:
- = ci_label_for_status(@commit.status)
- %span.light Authored by
- %strong
- = commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
- if @commit.different_committer?
.commit-info-row
@@ -41,8 +39,9 @@
#{time_ago_with_tooltip(@commit.committed_date)}
.commit-info-row
- %span.light Commit
- = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
+ %span.hidden-xs.hidden-sm Commit
+ = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm"
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline"
= clipboard_button(clipboard_text: @commit.id)
%span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent|
@@ -51,12 +50,23 @@
%span.commit-info.branches
%i.fa.fa-spinner.fa-spin
+- if @commit.status
+ .commit-info-row
+ Builds for
+ = pluralize(@commit.pipelines.count, 'pipeline')
+ = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
+ = ci_icon_for_status(@commit.status)
+ = ci_label_for_status(@commit.status)
+ - if @commit.pipelines.duration
+ in
+ = time_interval_in_words @commit.pipelines.duration
+
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line
+ = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
+ = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
:javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
new file mode 100644
index 00000000000..0411137b7c6
--- /dev/null
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -0,0 +1,52 @@
+.row-content-block.build-content.middle-block
+ .pull-right
+ - if can?(current_user, :update_pipeline, pipeline.project)
+ - if pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+
+ - if pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+
+ .oneline.clearfix
+ - if defined?(pipeline_details) && pipeline_details
+ Pipeline
+ = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
+ with
+ = pluralize pipeline.statuses.count(:id), "build"
+ - if pipeline.ref
+ for
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
+ - if pipeline.duration
+ in
+ = time_interval_in_words pipeline.duration
+
+- if pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+.table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Name
+ %th Tags
+ %th Duration
+ %th Finished at
+ - if pipeline.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ - pipeline.statuses.stages.each do |stage|
+ = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage)
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 7118a4846c6..2f051fb90e0 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,7 +1,7 @@
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
-= render "projects/commits/header_title"
+
.prepend-top-default
= render "commit_box"
-= render "ci_menu"
+= render "ci_menu"
= render "builds"
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index e5e3d696035..d0da2606587 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,16 +1,13 @@
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
-= render "projects/commits/header_title"
-
.prepend-top-default
= render "commit_box"
- if @commit.status
= render "ci_menu"
- else
%div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs, project: @project,
- diff_refs: @diff_refs
+= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder
new file mode 100644
index 00000000000..1657fb46163
--- /dev/null
+++ b/app/views/projects/commits/_commit.atom.builder
@@ -0,0 +1,14 @@
+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.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
+ xml.email commit.author_email
+ end
+
+ xml.summary markdown(commit.description, pipeline: :single_line)
+end
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c7d8c9a0d15..929496f81d8 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,26 +9,31 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .commit-row-title
- %span.item-title
- = 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 ...
+ = commit_author_avatar(commit, size: 36)
+ .commit-info-block
+ .commit-row-title
+ %span.item-title
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status
+ = render_commit_status(commit, cssclass: 'visible-xs-inline')
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- .pull-right
- - if commit.status
- = render_ci_status(commit)
- = clipboard_button(clipboard_text: commit.id)
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ .commit-actions.hidden-xs
+ - if commit.status
+ = render_commit_status(commit, cssclass: 'btn btn-transparent')
+ = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent')
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
- - if commit.description?
- .commit-row-description.js-toggle-content
- %pre
- = preserve(markdown(escape_once(commit.description), pipeline: :single_line))
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content
+ = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
- .commit-row-info
- by
- = commit_author_link(commit, avatar: true, size: 24)
- .committed_ago
- #{time_ago_with_tooltip(commit.committed_date)} &nbsp;
- = link_to_browse_code(project, commit)
+ .commit-row-info
+ = commit_author_link(commit, avatar: false, size: 24)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 64e8da9201d..dd12eae8f7e 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -3,19 +3,12 @@
- 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.strftime('%d %b, %Y')
- .light
- = pluralize(commits.count, 'commit')
- .col-md-10.col-sm-12
- %ul.content-list
- = render commits, project: project
- %hr.lists-separator
+- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
+ %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}"
+ %li.commits-row
+ %ul.list-unstyled.commit-list
+ = render commits, project: project
- if hidden > 0
- .alert.alert-warning
+ %li.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 d1bd76ab529..61152649907 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,24 +1,30 @@
-%ul.nav-links
- = nav_link(controller: [:commit, :commits]) do
- = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- Commits
- %span.badge
- = number_with_delimiter(@repository.commit_count)
+.scrolling-tabs-container.sub-nav-scroll
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
+ = link_to project_files_path(@project) do
+ Files
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
- Network
+ = nav_link(controller: [:commit, :commits]) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+ Commits
- = nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
- Compare
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+ Network
- = 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.branch_count
+ = nav_link(controller: :compare) do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
+ Compare
- = nav_link(controller: [:tags, :releases]) do
- = link_to namespace_project_tags_path(@project.namespace, @project) do
- Tags
- %span.badge.js-totaltags-count= @repository.tag_count
+ = nav_link(html_options: {class: branches_tab_class}) do
+ = link_to namespace_project_branches_path(@project.namespace, @project) do
+ Branches
+
+ = nav_link(controller: [:tags, :releases]) do
+ = link_to namespace_project_tags_path(@project.namespace, @project) do
+ Tags
diff --git a/app/views/projects/commits/_header_title.html.haml b/app/views/projects/commits/_header_title.html.haml
deleted file mode 100644
index e4385893dd9..00000000000
--- a/app/views/projects/commits/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Commits", project_commits_path(@project))
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index e310fafd82c..30bb7412073 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -6,18 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
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.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
- xml.email commit.author_email
- end
- xml.summary markdown(commit.description, pipeline: :single_line)
- end
- end
+ xml << render(@commits) if @commits.any?
end
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 088eaa28013..9a44ba94970 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,42 +1,41 @@
+- @no_container = true
+
- page_title "Commits", @ref
-= render "header_title"
= 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 "head"
-.row-content-block.second-block
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'commits'
-
- .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 }
+%div{ class: container_class }
+ .row-content-block.second-block.content-component-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'commits'
+
+ .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
- - if current_user && current_user.private_token
.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.content_list= render "commits", project: @project
-.clear
-= spinner
+ = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ - if current_user && current_user.private_token
+ .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)}
+ %ol#commits-list.list-unstyled.content_list
+ = render "commits", project: @project
+ = spinner
:javascript
CommitsList.init(#{@limit});
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 5e188dd0f3c..e9ff8e90dd5 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,17 +1,18 @@
+- @no_container = true
- page_title "Compare"
-= render "projects/commits/header_title"
= render "projects/commits/head"
-.row-content-block
- Compare branches, tags or commit ranges.
- %br
- Fill input field with commit id like
- %code.label-branch 4eedf23
- or branch/tag name like
- %code.label-branch master
- and press compare button for the commits list and a code diff.
- %br
- Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
+%div{ class: container_class }
+ .sub-header-block
+ Compare branches, tags or commit ranges.
+ %br
+ Fill input field with commit id like
+ %code.label-branch 4eedf23
+ or branch/tag name like
+ %code.label-branch master
+ and press compare button for the commits list and a code diff.
+ %br
+ Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
-.prepend-top-20
- = render "form"
+ .prepend-top-20
+ = render "form"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 62525168239..28a50e7031a 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,25 +1,24 @@
+- @no_container = true
- page_title "#{params[:from]}...#{params[:to]}"
-= render "projects/commits/header_title"
= render "projects/commits/head"
+%div{ class: container_class }
+ .sub-header-block.no-bottom-space
+ = render "form"
-.row-content-block
- = render "form"
-
-- if @commits.present?
- .prepend-top-default
+ - if @commits.present?
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
-- else
- .light-well.prepend-top-default
- .center
- %h4
- There isn't anything to compare.
- %p.slead
- - if params[:to] == params[:from]
- %span.label-branch #{params[:from]}
- and
- %span.label-branch #{params[:to]}
- are the same.
- - else
- You'll need to use different branch names to get a valid comparison.
+ - else
+ .light-well
+ .center
+ %h4
+ There isn't anything to compare.
+ %p.slead
+ - if params[:to] == params[:from]
+ %span.label-branch #{params[:from]}
+ and
+ %span.label-branch #{params[:to]}
+ are the same.
+ - else
+ You'll need to use different branch names to get a valid comparison.
diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml
new file mode 100644
index 00000000000..10822b6184c
--- /dev/null
+++ b/app/views/projects/container_registry/_tag.html.haml
@@ -0,0 +1,29 @@
+%tr.tag
+ %td
+ = escape_once(tag.name)
+ = clipboard_button(clipboard_text: "docker pull #{tag.path}")
+ %td
+ - if tag.revision
+ %span.has-tooltip{ title: "#{tag.revision}" }
+ = tag.short_revision
+ - else
+ \-
+ %td
+ - if tag.total_size
+ = number_to_human_size(tag.total_size)
+ &middot;
+ = pluralize(tag.layers.size, "layer")
+ - else
+ .light
+ \-
+ %td
+ - if tag.created_at
+ = time_ago_in_words(tag.created_at)
+ - else
+ .light
+ \-
+ - if can?(current_user, :update_container_image, @project)
+ %td.content
+ .controls.hidden-xs.pull-right
+ = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do
+ = icon("trash cred")
diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml
new file mode 100644
index 00000000000..993da27310f
--- /dev/null
+++ b/app/views/projects/container_registry/index.html.haml
@@ -0,0 +1,39 @@
+- page_title "Container Registry"
+
+%hr
+
+%ul.content-list
+ %li.light.prepend-top-default
+ %p
+ A 'container image' is a snapshot of a container.
+ You can host your container images with GitLab.
+ %br
+ To start using container images hosted on GitLab you first need to login:
+ %pre
+ %code
+ docker login #{Gitlab.config.registry.host_port}
+ %br
+ Then you are free to create and upload a container image with build and push commands:
+ %pre
+ docker build -t #{escape_once(@project.container_registry_repository_url)} .
+ %br
+ docker push #{escape_once(@project.container_registry_repository_url)}
+
+ - if @tags.blank?
+ %li
+ .nothing-here-block No images in Container Registry for this project.
+
+ - else
+ .table-holder
+ %table.table.tags
+ %thead
+ %tr
+ %th Name
+ %th Image ID
+ %th Size
+ %th Created
+ - if can?(current_user, :update_container_image, @project)
+ %th
+
+ - @tags.each do |tag|
+ = render 'tag', tag: tag
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
index e230834e8ba..04fbb37d93f 100644
--- a/app/views/projects/deploy_keys/index.html.haml
+++ b/app/views/projects/deploy_keys/index.html.haml
@@ -19,7 +19,7 @@
%ul.well-list
= render @enabled_keys
- else
- .profile-settings-message.text-center
+ .settings-message.text-center
No deploy keys found. Create one with the form above or add existing one below.
%h5.prepend-top-default
Deploy keys from projects you have access to (#{@available_project_keys.size})
@@ -27,7 +27,7 @@
%ul.well-list
= render @available_project_keys
- else
- .profile-settings-message.text-center
+ .settings-message.text-center
No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- if @available_public_keys.any?
%h5.prepend-top-default
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
new file mode 100644
index 00000000000..0f9d9512d88
--- /dev/null
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -0,0 +1,12 @@
+%div.branch-commit
+ - if deployment.ref
+ = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace"
+ &middot;
+ = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
+
+ %p.commit-title
+ %span
+ - if commit_title = deployment.commit_title
+ = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message"
+ - else
+ Cant find HEAD commit for this branch
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
new file mode 100644
index 00000000000..d08dd92f1f6
--- /dev/null
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -0,0 +1,23 @@
+%tr.deployment
+ %td
+ %strong= "##{deployment.iid}"
+
+ %td
+ = render 'projects/deployments/commit', deployment: deployment
+
+ %td
+ - if deployment.deployable
+ = link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do
+ = "#{deployment.deployable.name} (##{deployment.deployable.id})"
+
+ %td
+ #{time_ago_with_tooltip(deployment.created_at)}
+
+ %td
+ - if can?(current_user, :create_deployment, deployment) && deployment.deployable
+ .pull-right
+ = link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do
+ - if deployment.last?
+ Retry
+ - else
+ Rollback
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index d9c4b410d32..1975287faee 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -2,7 +2,7 @@
- if diff_view == 'parallel'
- fluid_layout true
-- diff_files = safe_diff_files(diffs, diff_refs)
+- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository)
.content-block.oneline-block.files-changed
.inline-parallel-buttons
@@ -11,6 +11,8 @@
= commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs')
- elsif current_controller?(:merge_requests)
= diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs')
+ - elsif current_controller?(:compare)
+ = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs')
.btn-group
= inline_diff_btn
= parallel_diff_btn
@@ -22,8 +24,9 @@
.files
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- - blob = project.repository.blob_for_diff(diff_commit, diff_file)
+ - blob = diff_file.blob(diff_commit)
- next unless blob
+ - blob.load_all_data!(project.repository) unless blob.only_display_raw?
= render 'projects/diffs/file', i: index, project: project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0f04fc5d33c..3b758a1ec4e 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,31 +1,8 @@
-.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
+.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)}
.file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- - if diff_file.diff.submodule?
- %span
- = icon('archive fw')
- %span
- = submodule_link(blob, @commit.id, project.repository)
- - else
- = blob_icon blob.mode, blob.name
-
- = 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)
- .filename.old
- = old_path
- &rarr;
- .filename.new
- = new_path
- - else
- %span
- = 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}"
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}"
+ - unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
@@ -41,18 +18,26 @@
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
- - return unless blob.respond_to?('text?')
+ - return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob)
- .nothing-here-block This diff was suppressed by a .gitattributes entry.
+ - elsif blob.only_display_raw?
+ .nothing-here-block This file is too large to display.
- elsif blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - if !project.repository.diffable?(blob)
+ .nothing-here-block This diff was suppressed by a .gitattributes entry.
+ - elsif diff_file.diff_lines.length > 0
+ - 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
- else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - if diff_file.mode_changed?
+ .nothing-here-block File mode changed
+ - elsif diff_file.renamed_file
+ .nothing-here-block File moved
- elsif blob.image?
- - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs
+ - old_blob = diff_file.old_blob(diff_commit)
+ = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
new file mode 100644
index 00000000000..95a2772fd0b
--- /dev/null
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -0,0 +1,25 @@
+- if defined?(blob) && blob && diff_file.submodule?
+ %span
+ = icon('archive fw')
+ %span
+ = submodule_link(blob, diff_commit.id, project.repository)
+- else
+ = conditional_link_to url.present?, url do
+ = blob_icon diff_file.b_mode, diff_file.file_path
+
+ - if diff_file.renamed_file
+ - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ %strong
+ = old_path
+ &rarr;
+ %strong
+ = new_path
+ - else
+ %strong
+ = diff_file.new_path
+ - if diff_file.deleted_file
+ deleted
+
+ - if diff_file.mode_changed?
+ %small
+ = "#{diff_file.a_mode} → #{diff_file.b_mode}"
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 2731219ccad..9ec6a7aa5cd 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,9 +1,8 @@
- diff = diff_file.diff
-- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
+- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))
// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
-- if diff_refs
- - old_commit_id = diff_refs.first.id
- - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
+- if diff_file.old_ref
+ - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
@@ -16,7 +15,7 @@
%div.two-up.view
%span.wrap
.frame.deleted
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))}
+ %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))}
%img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
@@ -28,7 +27,7 @@
%span.meta-height
%span.wrap
.frame.added
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))}
+ %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))}
%img{src: file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 107097ad963..22cad00240a 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,3 +1,6 @@
+- plain = local_assigns.fetch(:plain, false)
+- line_code = diff_file.line_code(line)
+- position = diff_file.position(line)
- type = line.type
%tr.line_holder{ id: line_code, class: type }
- case type
@@ -9,18 +12,19 @@
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
+ %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
- link_text = type == "new" ? "&nbsp;".html_safe : line.old_pos
- - if defined?(plain) && plain
+ - if plain
= link_text
- else
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
- - if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code)
+
+ - if !plain && !@diff_notes_disabled && can?(current_user, :create_note, @project)
+ = link_to_new_diff_note(line_code, position)
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? "&nbsp;".html_safe : line.new_pos
- - if defined?(plain) && plain
+ - if plain
= link_text
- else
= link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
- %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type)
+ %td.line_content.noteable_line{ class: [type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type)
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 81948513e43..51f207dce94 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -14,30 +14,28 @@
%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]} #{'empty-cell' if !left[:number]}"}
+ %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])]}
= 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(left[:line_code], 'old')
- %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
+ - if !@diff_notes_disabled && can?(current_user, :create_note, @project)
+ = link_to_new_diff_note(left[:line_code], left[:position], 'old')
+ %td.line_content.parallel.noteable_line{class: [left[:type], left[:line_code], ('empty-cell' if left[:text].empty?)], data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text])
- if right[:type] == 'new'
- new_line_class = 'new'
- new_line_code = right[:line_code]
+ - new_position = right[:position]
- else
- new_line_class = nil
- new_line_code = left[:line_code]
+ - new_position = left[:position]
- %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }}
+ %td.new_line.diff-line-num{id: new_line_code, class: [new_line_class, ('empty-cell' unless right[:number])], 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(right[:line_code], 'new')
- %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
+ - if !@diff_notes_disabled && can?(current_user, :create_note, @project)
+ = link_to_new_diff_note(new_line_code, new_position, 'new')
+ %td.line_content.parallel.noteable_line{class: [new_line_class, new_line_code, ('empty-cell' if right[:text].empty?)], data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text])
- - if @reply_allowed
- - 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
-
-- if diff_file.diff.diff.blank? && diff_file.mode_changed?
- .file-mode-changed
- File mode changed
+ - unless @diff_notes_disabled
+ - notes_left, notes_right = organize_comments(left, right)
+ - if notes_left.present? || notes_right.present?
+ = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index e7169d7b599..192093d1273 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,23 +4,17 @@
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' }
-
- last_line = 0
- - raw_diff_lines = diff_file.diff_lines.to_a
- - diff_file.highlighted_diff_lines.each_with_index do |line, index|
- - line_code = generate_line_code(diff_file.file_path, line)
+ - diff_file.highlighted_diff_lines.each do |line|
- last_line = line.new_pos
- = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code}
+ = render "projects/diffs/line", line: line, diff_file: diff_file
- - 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: raw_diff_lines[index].text
+ - unless @diff_notes_disabled
+ - line_code = diff_file.line_code(line)
+ - diff_notes = @grouped_diff_notes[line_code] if line_code
+ - if diff_notes
+ = render "projects/notes/diff_notes_with_reply", notes: diff_notes
- 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 }
-
-- if diff_file.diff.blank? && diff_file.mode_changed?
- .file-mode-changed
- File mode changed
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 76a4f41193c..27a94fe02dc 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,251 +1,259 @@
-.project-edit-container.prepend-top-default
- .project-edit-errors
- .project-edit-content
- .panel.panel-default
- .panel-heading
+.project-edit-container
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Project settings
- .panel-body
- = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f|
-
- %fieldset
- .form-group.project_name_holder
- = f.label :name, class: 'control-label' do
- Project name
- .col-sm-10
- = f.text_field :name, class: "form-control", id: "project_name_edit"
-
-
- .form-group
- = f.label :description, class: 'control-label' do
- Project description
- %span.light (optional)
- .col-sm-10
- = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
-
- - unless @project.empty_repo?
- .form-group
- = f.label :default_branch, "Default Branch", class: 'control-label'
- .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
-
-
- = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project
-
+ .col-lg-9
+ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
+ %fieldset.append-bottom-0
.form-group
- = f.label :tag_list, "Tags", class: 'control-label'
- .col-sm-10
- = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
- %p.help-block Separate tags with commas.
-
- %fieldset.features
- %legend
- Features:
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :issues_enabled do
- = f.check_box :issues_enabled
- %strong Issues
- %br
- %span.descr Lightweight issue tracking system for this project
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :merge_requests_enabled do
- = f.check_box :merge_requests_enabled
- %strong Merge Requests
- %br
- %span.descr Submit changes to be merged upstream
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :builds_enabled do
- = f.check_box :builds_enabled
- %strong Builds
- %br
- %span.descr Test and deploy your changes before merge
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :wiki_enabled do
- = f.check_box :wiki_enabled
- %strong Wiki
- %br
- %span.descr Pages for project documentation
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :snippets_enabled do
- = f.check_box :snippets_enabled
- %strong Snippets
- %br
- %span.descr Share code pastes with others out of git repository
-
- = render 'builds_settings', f: f
+ = f.label :name, class: 'label-light' do
+ Project name
+ = f.text_field :name, class: "form-control", id: "project_name_edit"
+ .form-group
+ = f.label :description, class: 'label-light' do
+ Project description
+ %span.light (optional)
+ = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
- %fieldset.features
- %legend
- Project avatar:
+ - unless @project.empty_repo?
.form-group
- .col-sm-offset-2.col-sm-10
- - if @project.avatar?
- = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
- %p.light
- - if @project.avatar_in_git
- Project avatar in repository: #{ @project.avatar_in_git }
- %p.light
- - if @project.avatar?
- You can change your project avatar here
- - else
- You can upload a project avatar here
- %a.choose-btn.btn.btn-sm.js-choose-project-avatar-button
- %i.icon-paper-clip
- %span Choose File ...
- &nbsp;
- %span.file_name.js-avatar-filename File name...
- = f.file_field :avatar, class: "js-project-avatar-input hidden"
- .light The maximum file size allowed is 200KB.
- - if @project.avatar?
- %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"
-
-
- .form-actions
- = f.submit 'Save changes', class: "btn btn-save"
-
-
-
- .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
- .panel-heading
- Unarchive project
- .panel-body
- %p
- Unarchiving the project will mark its repository as active.
+ = f.label :default_branch, "Default Branch", class: 'label-light'
+ = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
+ .form-group.project-visibility-level-holder
+ = f.label :visibility_level, class: 'label-light' do
+ Visibility Level
+ = link_to "(?)", help_page_path("public_access", "public_access")
+ - if can_change_visibility_level?(@project, current_user)
+ = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
+ - else
+ .info
+ = visibility_level_icon(@project.visibility_level)
+ %strong
+ = visibility_level_label(@project.visibility_level)
+ .light= visibility_level_description(@project.visibility_level, @project)
+ .form-group
+ = f.label :tag_list, "Tags", class: 'label-light'
+ = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
+ %p.help-block Separate tags with commas.
+ %hr
+ %fieldset.features.append-bottom-0
+ %h5.prepend-top-0
+ Features
+ .form-group
+ .checkbox
+ = f.label :issues_enabled do
+ = f.check_box :issues_enabled
+ %strong Issues
+ %br
+ %span.descr Lightweight issue tracking system for this project
+ .form-group
+ .checkbox
+ = f.label :merge_requests_enabled do
+ = f.check_box :merge_requests_enabled
+ %strong Merge Requests
%br
- The project can be committed to.
+ %span.descr Submit changes to be merged upstream
+ .form-group
+ .checkbox
+ = f.label :builds_enabled do
+ = f.check_box :builds_enabled
+ %strong Builds
%br
- %strong Once active this project shows up in the search and on the dashboard.
-
- .form-actions
- = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project),
- data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
- method: :post, class: "btn btn-success"
- - else
- .panel.panel-warning
- .panel-heading
- Archive project
- .panel-body
- %p
- Archiving the project will mark its repository as read-only.
+ %span.descr Test and deploy your changes before merge
+ .form-group
+ .checkbox
+ = f.label :wiki_enabled do
+ = f.check_box :wiki_enabled
+ %strong Wiki
%br
- It is hidden from the dashboard and doesn't show up in searches.
+ %span.descr Pages for project documentation
+ .form-group
+ .checkbox
+ = f.label :snippets_enabled do
+ = f.check_box :snippets_enabled
+ %strong Snippets
%br
- %strong Archived projects cannot be committed to!
-
- .form-actions
- = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project),
- data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
- method: :post, class: "btn btn-warning"
- - else
- .nothing-here-block Only the project owner can archive a project
-
- .panel.panel-default.panel.panel-warning
- .panel-heading Rename repository
- .errors-holder
- .panel-body
- = form_for([@project.namespace.becomes(Namespace), @project], html: { class: 'form-horizontal' }) do |f|
- .form-group.project_name_holder
- = f.label :name, class: 'control-label' do
- Project name
- .col-sm-9
- .form-group
- = f.text_field :name, class: "form-control"
+ %span.descr Share code pastes with others out of git repository
+ - if Gitlab.config.registry.enabled
.form-group
- = f.label :path, class: 'control-label' do
- %span Path
- .col-sm-9
- .form-group
- .input-group
- .input-group-addon
- #{URI.join(root_url, @project.namespace.path)}/
- = f.text_field :path, class: 'form-control'
- %ul
- %li Be careful. Renaming a project's repository can have unintended side effects.
- %li You will need to update your local repositories to point to the new location.
- .form-actions
- = f.submit 'Rename project', class: "btn btn-warning"
-
- - if can?(current_user, :change_namespace, @project)
- .panel.panel-default.panel.panel-danger
- .panel-heading Transfer project
- .errors-holder
- .panel-body
- = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
- .form-group
- = label_tag :new_namespace_id, nil, class: 'control-label' do
- %span Namespace
- .col-sm-9
- .form-group
- = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' }
- %ul
- %li Be careful. Changing the project's namespace can have unintended side effects.
- %li You can only transfer the project to namespaces you manage.
- %li You will need to update your local repositories to point to the new location.
- %li Project visibility level will be changed to match namespace rules when transfering to a group.
- .form-actions
- = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- - else
- .nothing-here-block Only the project owner can transfer a project
-
- - if @project.forked?
- - if can?(current_user, :remove_fork_project, @project)
- = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
- .panel.panel-default.panel.panel-danger
- .panel-heading Remove fork relationship
- .panel-body
- %p
- This will remove the fork relationship to source project
- #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}.
+ .checkbox
+ = f.label :container_registry_enabled do
+ = f.check_box :container_registry_enabled
+ %strong Container Registry
%br
- %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
- .form-actions
- = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
- - else
- .nothing-here-block Only the project owner can remove the fork relationship.
+ %span.descr Enable Container Registry for this repository
+ %hr
+ = render 'merge_request_settings', f: f
+ %hr
+ = render 'builds_settings', f: f
+ %hr
+ %fieldset.features.append-bottom-default
+ %h5.prepend-top-0
+ Project avatar
+ .form-group
+ - if @project.avatar?
+ = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+ %p.light
+ - if @project.avatar_in_git
+ Project avatar in repository: #{ @project.avatar_in_git }
+ %a.choose-btn.btn.js-choose-project-avatar-button
+ Browse file...
+ %span.file_name.prepend-left-default.js-avatar-filename No file chosen
+ = f.file_field :avatar, class: "js-project-avatar-input hidden"
+ .help-block The maximum file size allowed is 200KB.
+ - if @project.avatar?
+ %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"
+ = f.submit 'Save changes', class: "btn btn-save"
+ .row.prepend-top-default
+ %hr
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Housekeeping
+ %p.append-bottom-0
+ %p
+ Runs a number of housekeeping tasks within the current repository,
+ such as compressing file revisions and removing unreachable objects.
+ .col-lg-9
+ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-save"
+ %hr
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Export project
+ %p.append-bottom-0
+ %p
+ Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
+ %p
+ Once the exported file is ready, you will receive a notification email with a download link.
- - if can?(current_user, :remove_project, @project)
- .panel.panel-default.panel.panel-danger
- .panel-heading Remove project
- .panel-body
- = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do
- %p
- Removing the project will delete its repository and all related resources including issues, merge requests etc.
- %br
- %strong Removed projects cannot be restored!
- .form-actions
- = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
+ .col-lg-9
+
+ - if @project.export_project_path
+ = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
+ method: :get, class: "btn btn-default"
+ = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
- else
- .nothing-here-block Only the project owner can remove a project.
+ = link_to 'Export project', export_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+ .bs-callout.bs-callout-info
+ %p.append-bottom-0
+ %p
+ The following items will be exported:
+ %ul
+ %li Project and wiki repositories
+ %li Project uploads
+ %li Project configuration including web hooks and services
+ %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %p
+ The following items will NOT be exported:
+ %ul
+ %li Build traces and artifacts
+ %li LFS objects
+ %hr
+ - if can? current_user, :archive_project, @project
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.warning-title.prepend-top-0
+ - if @project.archived?
+ Unarchive project
+ - else
+ Archive project
+ %p.append-bottom-0
+ - if @project.archived?
+ Unarchiving the project will mark its repository as active. The project can be committed to.
+ - else
+ Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
+ .col-lg-9
+ - if @project.archived?
+ %p
+ %strong Once active this project shows up in the search and on the dashboard.
+ = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project),
+ data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
+ method: :post, class: "btn btn-success"
+ - else
+ %p
+ %strong Archived projects cannot be committed to!
+ = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project),
+ data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
+ method: :post, class: "btn btn-warning"
+ %hr
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0.warning-title
+ Rename repository
+ .col-lg-9
+ = form_for([@project.namespace.becomes(Namespace), @project]) do |f|
+ .form-group.project_name_holder
+ = f.label :name, class: 'label-light' do
+ Project name
+ .form-group
+ = f.text_field :name, class: "form-control"
+ .form-group
+ = f.label :path, class: 'label-light' do
+ %span Path
+ .form-group
+ .input-group
+ .input-group-addon
+ #{URI.join(root_url, @project.namespace.path)}/
+ = f.text_field :path, class: 'form-control'
+ %ul
+ %li Be careful. Renaming a project's repository can have unintended side effects.
+ %li You will need to update your local repositories to point to the new location.
+ = f.submit 'Rename project', class: "btn btn-warning"
+ - if can?(current_user, :change_namespace, @project)
+ %hr
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0.danger-title
+ Transfer project
+ .col-lg-9
+ = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true) do |f|
+ .form-group
+ = label_tag :new_namespace_id, nil, class: 'label-light' do
+ %span Namespace
+ .form-group
+ = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' }
+ %ul
+ %li Be careful. Changing the project's namespace can have unintended side effects.
+ %li You can only transfer the project to namespaces you manage.
+ %li You will need to update your local repositories to point to the new location.
+ %li Project visibility level will be changed to match namespace rules when transfering to a group.
+ = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ - if @project.forked? && can?(current_user, :remove_fork_project, @project)
+ %hr
+ .row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0.danger-title
+ Remove fork relationship
+ %p.append-bottom-0
+ %p
+ This will remove the fork relationship to source project
+ = succeed "." do
+ = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
+ .col-lg-9
+ = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
+ %p
+ %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
+ = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) }
+ - if can?(current_user, :remove_project, @project)
+ %hr
+ .row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0.danger-title
+ Remove project
+ %p.append-bottom-0
+ Removing the project will delete its repository and all related resources including issues, merge requests etc.
+ .col-lg-9
+ = form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do
+ %p
+ %strong Removed projects cannot be restored!
+ = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
.save-project-loader.hide
.center
@@ -254,5 +262,4 @@
Saving project.
%p Please wait a moment, this page will automatically refresh when ready.
-
= render 'shared/confirm_modal', phrase: @project.path
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 1a2e59752fe..636beb73ec2 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -15,10 +15,14 @@
If you already have files you can push them using command line instructions below.
%p
Otherwise you can start with adding a
- = link_to "README", new_readme_path, class: 'underlined-link'
+ = succeed ',' do
+ = link_to "README", new_readme_path, class: 'underlined-link'
+ a
+ = succeed ',' do
+ = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
or a
- = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
- file to this project.
+ = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
+ to this project.
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
new file mode 100644
index 00000000000..eafa246d05f
--- /dev/null
+++ b/app/views/projects/environments/_environment.html.haml
@@ -0,0 +1,17 @@
+- last_deployment = environment.last_deployment
+
+%tr.environment
+ %td
+ %strong
+ = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+
+ %td
+ - if last_deployment
+ = render 'projects/deployments/commit', deployment: last_deployment
+ - else
+ %p.commit-title
+ No deployments yet
+
+ %td
+ - if last_deployment
+ #{time_ago_with_tooltip(last_deployment.created_at)}
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
new file mode 100644
index 00000000000..c07f4bd510c
--- /dev/null
+++ b/app/views/projects/environments/_form.html.haml
@@ -0,0 +1,7 @@
+= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f|
+ = form_errors(@environment)
+ .form-group
+ = f.label :name, 'Name', class: 'label-light'
+ = f.text_field :name, required: true, class: 'form-control'
+ = f.submit 'Create environment', class: 'btn btn-create'
+ = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml
new file mode 100644
index 00000000000..e056fccad5d
--- /dev/null
+++ b/app/views/projects/environments/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Environments", project_environments_path(@project))
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
new file mode 100644
index 00000000000..5242021243e
--- /dev/null
+++ b/app/views/projects/environments/index.html.haml
@@ -0,0 +1,31 @@
+- @no_container = true
+- page_title "Environments"
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ - if can?(current_user, :create_environment, @project) && !@environments.blank?
+ .top-area
+ .nav-controls
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+
+ - if @environments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any environments right now.
+ %p.blank-state-text
+ Environments are places where code gets deployed, such as staging or production.
+ %br
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci", "environments")
+ - if can?(current_user, :create_environment, @project)
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+ - else
+ .table-holder
+ %table.table.environments
+ %tbody
+ %th Environment
+ %th Last deployment
+ %th Date
+ = render @environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
new file mode 100644
index 00000000000..da325efecd2
--- /dev/null
+++ b/app/views/projects/environments/new.html.haml
@@ -0,0 +1,12 @@
+- page_title 'New Environment'
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ New Environment
+ %p
+ Environments allow you to track deployments of your application
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci", "environments")
+
+ = render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
new file mode 100644
index 00000000000..53c62ef234d
--- /dev/null
+++ b/app/views/projects/environments/show.html.haml
@@ -0,0 +1,37 @@
+- @no_container = true
+- page_title "Environments"
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ .top-area
+ .col-md-9
+ %h3.page-title= @environment.name.titleize
+
+ .col-md-3
+ .nav-controls
+ - if can?(current_user, :update_environment, @environment)
+ = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
+
+ - if @deployments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ %table.table.environments
+ %thead
+ %tr
+ %th ID
+ %th Commit
+ %th Build
+ %th Date
+ %th
+
+ = render @deployments
+
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 1fe1d98bf13..9322c82904f 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,5 +1,4 @@
- page_title "Find File", @ref
-- header_title project_title(@project, "Files", project_files_path(@project))
.file-finder-holder.tree-holder.clearfix
.nav-block
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 4bcf2d9d533..dbe9ddfde2f 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -40,9 +40,3 @@
= 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/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index f21c864e35c..5bc5c71283e 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -12,6 +12,9 @@
- else
%strong ##{generic_commit_status.id}
+ - if defined?(retried) && retried
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
+
- 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"
@@ -37,11 +40,13 @@
%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
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if defined?(retried) && retried
+ %span.label.label-warning retried
%td.duration
- if generic_commit_status.duration
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 79a56647c53..ca347406dfe 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,11 +1,16 @@
-%ul.nav-links
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/application.js')
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.builds_enabled?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/_header_title.html.haml b/app/views/projects/graphs/_header_title.html.haml
deleted file mode 100644
index 1e2f61cd22b..00000000000
--- a/app/views/projects/graphs/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Graphs", namespace_project_graph_path(@project.namespace, @project, current_ref))
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
index 9f05be9982b..6be4273b6ab 100644
--- a/app/views/projects/graphs/ci.html.haml
+++ b/app/views/projects/graphs/ci.html.haml
@@ -1,16 +1,18 @@
+- @no_container = true
- page_title "Continuous Integration", "Graphs"
-= render "header_title"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- A collection of graphs for Continuous Integration
-#charts.ci-charts
- .row
- .col-md-6
- = render 'projects/graphs/ci/overall'
- .col-md-6
- = render 'projects/graphs/ci/build_times'
+%div{ class: container_class }
+ .sub-header-block
+ .oneline
+ A collection of graphs for Continuous Integration
- %hr
- = render 'projects/graphs/ci/builds'
+ #charts.ci-charts
+ .row
+ .col-md-6
+ = render 'projects/graphs/ci/overall'
+ .col-md-6
+ = render 'projects/graphs/ci/build_times'
+
+ %hr
+ = render 'projects/graphs/ci/builds'
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml
index 4b12e5f2da1..edc4f7b079f 100644
--- a/app/views/projects/graphs/ci/_overall.haml
+++ b/app/views/projects/graphs/ci/_overall.haml
@@ -16,4 +16,4 @@
%li
Commits covered:
%strong
- = @project.ci_commits.count(:all)
+ = @project.pipelines.count(:all)
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index da9f648cc9c..65db8af494d 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,53 +1,54 @@
+- @no_container = true
- page_title "Commits", "Graphs"
-= render "header_title"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+%div{ class: container_class }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
-%p.lead
- Commit statistics for
- %strong #{@ref}
- #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+ %p.lead
+ Commit statistics for
+ %strong #{@ref}
+ #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
-.row
- .col-md-6
- %ul
- %li
- %p.lead
- %strong #{@commits_graph.commits.size}
- commits during
- %strong #{@commits_graph.duration}
- days
- %li
- %p.lead
- Average
- %strong #{@commits_graph.commit_per_day}
- commits per day
- %li
- %p.lead
- Contributed by
- %strong #{@commits_graph.authors}
- authors
- .col-md-6
- %div
- %p.slead
- Commits per day of month
- %canvas#month-chart
-.row
- .col-md-6
- %div
- %p.slead
- Commits per day hour (UTC)
- %canvas#hour-chart
- .col-md-6
- %div
- %p.slead
- Commits per weekday
- %canvas#weekday-chart
+ .row
+ .col-md-6
+ %ul
+ %li
+ %p.lead
+ %strong #{@commits_graph.commits.size}
+ commits during
+ %strong #{@commits_graph.duration}
+ days
+ %li
+ %p.lead
+ Average
+ %strong #{@commits_graph.commit_per_day}
+ commits per day
+ %li
+ %p.lead
+ Contributed by
+ %strong #{@commits_graph.authors}
+ authors
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day of month
+ %canvas#month-chart
+ .row
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day hour (UTC)
+ %canvas#hour-chart
+ .col-md-6
+ %div
+ %p.slead
+ Commits per weekday
+ %canvas#weekday-chart
:javascript
var responsiveChart = function (selector, data) {
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
index ebecab1dbfc..fcfcae0be20 100644
--- a/app/views/projects/graphs/languages.html.haml
+++ b/app/views/projects/graphs/languages.html.haml
@@ -1,25 +1,26 @@
+- @no_container = true
- page_title "Languages", "Graphs"
-= render "header_title"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- Programming languages used in this repository
+%div{ class: container_class }
+ .sub-header-block
+ .oneline
+ Programming languages used in this repository
-.row
- .col-md-8
- %canvas#languages-chart{ height: 400 }
- .col-md-4
- %ul.bordered-list
- - @languages.each do |language|
- %li
- %span{ style: "color: #{language[:color]}" }
- = icon('circle')
- &nbsp;
- = language[:label]
- .pull-right
- = language[:value]
- \%
+ .row
+ .col-md-8
+ %canvas#languages-chart{ height: 400 }
+ .col-md-4
+ %ul.bordered-list
+ - @languages.each do |language|
+ %li
+ %span{ style: "color: #{language[:color]}" }
+ = icon('circle')
+ &nbsp;
+ = language[:label]
+ .pull-right
+ = language[:value]
+ \%
:javascript
var data = #{@languages.to_json};
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index ad4a932d391..a985b442b2d 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,30 +1,31 @@
+- @no_container = true
- page_title "Contributors", "Graphs"
-= render "header_title"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
-
-.loading-graph
- .center
- %h3.page-title
- %i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
-
-.stat-graph.hide
- .header.clearfix
- %h3#date_header.page-title
- %p.light
- Commits to #{@ref}, excluding merge commits. Limited by 6,000 commits
- %input#brush_change{:type => "hidden"}
- .graphs
- #contributors-master
- #contributors.clearfix
- %ol.contributors-list.clearfix
+%div{ class: container_class }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
+ .loading-graph
+ .center
+ %h3.page-title
+ %i.fa.fa-spinner.fa-spin
+ Building repository graph.
+ %p.slead Please wait a moment, this page will automatically refresh when ready.
+
+ .stat-graph.hide
+ .header.clearfix
+ %h3#date_header.page-title
+ %p.light
+ Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ %input#brush_change{:type => "hidden"}
+ .graphs.row
+ #contributors-master
+ #contributors.clearfix
+ %ol.contributors-list.clearfix
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 62eba5888a4..8151187d499 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 36c1d69f060..8faad351463 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,84 +1 @@
-- page_title "Webhooks"
-.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = page_title
- %p
- #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be
- used for binding events when something is happening within the project.
- .col-lg-9.append-bottom-default
- %h5.prepend-top-0
- Add new webhook
- = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project) do |f|
- = form_errors(@hook)
-
- .form-group
- = f.label :url, "URL", class: "label-light"
- = f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json"
- .form-group
- = f.label :token, "Secret Token", class: 'label-light'
- = f.text_field :token, class: "form-control", placeholder: ''
- %p.help-block
- Use this token to validate received payloads
- .form-group
- = f.label :url, "Trigger", class: "label-light"
- %div
- = f.check_box :push_events, class: "pull-left"
- .prepend-left-20
- = f.label :push_events, class: "label-light append-bottom-0" do
- Push events
- %p.light
- This url will be triggered by a push to the repository
- %div
- = f.check_box :tag_push_events, class: "pull-left"
- .prepend-left-20
- = f.label :tag_push_events, class: "label-light append-bottom-0" do
- Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- %div
- = f.check_box :note_events, class: "pull-left"
- .prepend-left-20
- = f.label :note_events, class: "label-light append-bottom-0" do
- Comments
- %p.light
- This url will be triggered when someone adds a comment
- %div
- = f.check_box :issues_events, class: "pull-left"
- .prepend-left-20
- = f.label :issues_events, class: "label-light append-bottom-0" do
- Issues events
- %p.light
- 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: "label-light append-bottom-0" do
- Merge Request events
- %p.light
- 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
- = f.label :build_events, class: "label-light append-bottom-0" do
- Build events
- %p.light
- This url will be triggered when the build status changes
- .form-group
- = f.label :enable_ssl_verification, "SSL verification", class: "label-light"
- %div
- = f.check_box :enable_ssl_verification, class: "pull-left"
- .prepend-left-20
- = f.label :enable_ssl_verification, class: "label-light append-bottom-0" do
- Enable SSL verification
- = f.submit "Add Webhook", class: "btn btn-create"
- %hr
- %h5.prepend-top-default
- Webhooks (#{@hooks.count})
- - if @hooks.any?
- %ul.well-list
- - @hooks.each do |hook|
- = render "project_hook", hook: hook
- - else
- %p.profile-settings-message.text-center.append-bottom-0
- No webhooks found, add one in the form above.
+= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project]
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index a8a8caf7280..2cd8d03e30e 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -10,7 +10,7 @@
.panel-body
%pre
:preserve
- #{sanitize_repo_path(@project.import_error)}
+ #{sanitize_repo_path(@project, @project.import_error)}
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
= render "shared/import_form", f: f
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index c0d1ce0d120..4d8ee562e6a 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -7,7 +7,7 @@
Forking in progress.
- else
Import in progress.
- - unless @project.forked?
+ - if @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index b151393abab..c2f4457b60b 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, 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'
+ = 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-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-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/_head.html.haml b/app/views/projects/issues/_head.html.haml
new file mode 100644
index 00000000000..403adb7426b
--- /dev/null
+++ b/app/views/projects/issues/_head.html.haml
@@ -0,0 +1,25 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do
+ %span
+ Issues
+
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
+
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
+
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/projects/issues/_header_title.html.haml b/app/views/projects/issues/_header_title.html.haml
deleted file mode 100644
index 99f03549c44..00000000000
--- a/app/views/projects/issues/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 5cf70ea3bb7..79b14819865 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,4 +1,4 @@
-%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) }
+%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
@@ -6,7 +6,7 @@
.issue-title.title
%span.issue-title-text
= confidential_icon(issue)
- = link_to_gfm issue.title, issue_path(issue)
+ = link_to issue.title, issue_path(issue)
%ul.controls
- if issue.closed?
%li
@@ -27,7 +27,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = issue.notes.user.nonawards.count
+ - note_count = issue.notes.user.count
%li
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
= icon('comments')
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d6b38b327ff..d8075371853 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,12 +2,12 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list
- - has_any_ci = @merge_requests.any?(&:ci_commit)
+ - has_any_ci = @merge_requests.any?(&:pipeline)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- - if merge_request.ci_commit
- = render_ci_status(merge_request.ci_commit)
+ - if merge_request.pipeline
+ = render_pipeline_status(merge_request.pipeline)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
@@ -25,4 +25,5 @@
- 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}
+ %li
+ = 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
index 469429ccf3c..e93b7e0d66d 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,13 +1,13 @@
- if can?(current_user, :push_code, @project)
.pull-right
#new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
- = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
+ method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
.checking
- %i.fa.fa-spinner.fa-spin
+ = icon('spinner spin')
Checking branches
- .available(style="display: none")
- %i.fa.fa-code-fork
+ .available.hide
New branch
- .unavailable(style="display: none")
- %i.fa.fa-exclamation-triangle
+ .unavailable.hide
+ = icon('exclamation-triangle')
New branch unavailable
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index bdfa0c7009e..c6fc499a7b8 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -5,10 +5,10 @@
- @related_branches.each do |branch|
%li
- sha = @project.repository.find_branch(branch).target
- - ci_commit = @project.ci_commit(sha, branch) if sha
- - if ci_commit
+ - pipeline = @project.pipeline(sha, branch) if sha
+ - if pipeline
%span.related-branch-ci-status
- = render_ci_status(ci_commit)
+ = render_pipeline_status(pipeline)
%span.related-branch-info
%strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 20216297d25..7cf1923456e 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
-= render "header_title"
%h3.page-title
Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index ee8a9414657..36957560de0 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,9 +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.xmlschema if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
- @issues.each do |issue|
- issue_to_atom(xml, issue)
- end
+ xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index efa7642b2dc..312bd86ed04 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,25 +1,42 @@
+- @no_container = true
- page_title "Issues"
-= render "header_title"
+= render "projects/issues/head"
= content_for :meta_tags do
- 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")
-.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')
- %span.icon-label
- Subscribe
- = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
- = icon('plus')
- New Issue
+%div{ class: (container_class) }
+ - if @project.issues.any?
+ .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')
+ %span.icon-label
+ Subscribe
+ = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
+ - if can? current_user, :create_issue, @project
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
+ New Issue
+ = render 'shared/issuable/filter', type: :issues
-= render 'shared/issuable/filter', type: :issues
-
-.issues-holder
- = render "issues"
+ .issues-holder
+ = render "issues"
+ - else
+ .blank-state.blank-state-welcome
+ %h2.blank-state-title.blank-state-welcome-title
+ Welcome to GitLab Issues
+ %p.blank-state-text
+ Code, test, and deploy together
+ .blank-state
+ .blank-state-icon
+ = navbar_icon("issues", size: 50)
+ %h3.blank-state-title
+ You don't have any issues right now.
+ %p.blank-state-text
+ Issues are the best way to track your project progress
+ - if can? current_user, :create_issue, @project
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
+ New Issue
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index b317a0c1cf4..e8aae0f47e2 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Issue"
-= render "header_title"
%h3.page-title
New Issue
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bde80bbb54b..9b6a97c0959 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,7 +1,6 @@
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
-- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
.clearfix.detail-page-header
.issuable-header
@@ -39,26 +38,24 @@
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
- = icon('plus')
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm 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: "hidden-xs hidden-sm 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: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do
- = icon('pencil-square-o')
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn 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: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
Edit
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@issue.title), pipeline: :single_line
+ = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
= preserve do
- = markdown(@issue.description, cache_key: [@issue, "description"])
+ = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
@@ -71,7 +68,7 @@
.content-block.content-block-small
= render 'new_branch'
- = render 'votes/votes_block', votable: @issue
+ = render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/labels/_header_title.html.haml b/app/views/projects/labels/_header_title.html.haml
deleted file mode 100644
index abe28da483b..00000000000
--- a/app/views/projects/labels/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Labels", namespace_project_labels_path(@project.namespace, @project))
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 8bf544b8371..73c6f2a046c 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -1,28 +1,50 @@
-%li{id: dom_id(label)}
+- label_css_id = dom_id(label)
+%li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
- .pull-info-right
- %span.append-right-20
- = link_to_label(label, type: :merge_request) do
- = pluralize label.open_merge_requests_count, 'merge request'
+ .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
+ %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
+ Options
+ %span.caret
+ .dropdown-menu.dropdown-menu-align-right
+ %ul
+ %li
+ = link_to_label(label, type: :merge_request) do
+ = pluralize label.open_merge_requests_count, 'merge request'
+ %li
+ = link_to_label(label) do
+ = pluralize label.open_issues_count(current_user), 'open issue'
+ - if current_user
+ %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span= label_subscription_toggle_button_text(label)
+ - if can? current_user, :admin_label, @project
+ %li
+ = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
+ %li
+ = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
- %span.append-right-20
- = link_to_label(label) do
- = pluralize label.open_issues_count(current_user), 'open issue'
+ .pull-right.hidden-xs.hidden-sm.hidden-md
+ = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = pluralize label.open_merge_requests_count, 'merge request'
+ = link_to_label(label, css_class: 'btn btn-transparent btn-action') do
+ = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user
- .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
- .subscription-status{data: {status: label_subscription_status(label)}}
-
- %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } }
- %span= label_subscription_toggle_button_text(label)
+ .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span.sr-only= label_subscription_toggle_button_text(label)
+ = icon('eye', class: 'label-subscribe-button-icon')
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
- if can? current_user, :admin_label, @project
- = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do
- %i.fa.fa-pencil-square-o
- = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
- %i.fa.fa-trash-o
+ = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
+ %span.sr-only Edit
+ = icon('pencil-square-o')
+ = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
+ %span.sr-only Delete
+ = icon('trash-o')
-- if current_user
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
+ - if current_user
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 675a805e12f..6901ba13ab7 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", @label.name, "Labels"
-= render "header_title"
%h3.page-title
Edit Label
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index cc41130a9dc..db66a0edbd8 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,23 +1,38 @@
+- @no_container = true
- page_title "Labels"
-= render "header_title"
+- hide_class = ''
+= render "projects/issues/head"
-.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
+%div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
-.labels
- - if @labels.present?
- %ul.content-list.manage-labels-list
- = render @labels
- = paginate @labels, theme: 'gitlab'
- - else
- .nothing-here-block
- - if can? current_user, :admin_label, @project
- Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels
+ .nav-controls
+ - if can?(current_user, :admin_label, @project)
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ New label
+
+ .labels
+ - if can?(current_user, :admin_label, @project)
+ -# Only show it in the first page
+ - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
+ .prioritized-labels{ class: ('hide' if hide) }
+ %h5 Prioritized Labels
+ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
+ %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
+ - if @prioritized_labels.present?
+ = render @prioritized_labels
+ .other-labels
+ - if can?(current_user, :admin_label, @project)
+ %h5{ class: ('hide' if hide) } Other Labels
+ - if @labels.present?
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render @labels
+ = paginate @labels, theme: 'gitlab'
- else
- No labels created
+ .nothing-here-block
+ - if can?(current_user, :admin_label, @project)
+ Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
+ - else
+ No labels created
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index e20fd7d6891..49ddf901619 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Label"
-= render "header_title"
%h3.page-title
New Label
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 393998f15b9..53dd300c35c 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', 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"}
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment 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', 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"}
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment 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/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml
deleted file mode 100644
index 19e4dab874b..00000000000
--- a/app/views/projects/merge_requests/_head.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.top-tabs
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab #{'active' if current_page?(namespace_project_merge_requests_path(@project.namespace, @project)) }" do
- %span
- Merge Requests
-
diff --git a/app/views/projects/merge_requests/_header_title.html.haml b/app/views/projects/merge_requests/_header_title.html.haml
deleted file mode 100644
index 669a9b06bdf..00000000000
--- a/app/views/projects/merge_requests/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 73c6a95f5ca..5029b365f93 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.title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request)
+ = link_to merge_request.title, merge_request_path(merge_request)
%ul.controls
- if merge_request.merged?
%li
@@ -11,9 +11,9 @@
= icon('ban')
CLOSED
- - if merge_request.ci_commit
+ - if merge_request.pipeline
%li
- = render_ci_status(merge_request.ci_commit)
+ = render_pipeline_status(merge_request.pipeline)
- if merge_request.open? && merge_request.broken?
%li
@@ -35,7 +35,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.nonawards.count
+ - note_count = merge_request.mr_and_commit_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
= icon('comments')
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 5473fa19166..446887774a4 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -6,4 +6,3 @@
- if @merge_requests.present?
= 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 b08524574e4..de39964fca8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 18b3f9e1549..a5e67b95727 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -23,7 +23,7 @@
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- - if @ci_commit
+ - if @pipeline
%li.builds-tab.active
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds
@@ -43,7 +43,7 @@
%p To preserve performance the line changes are not shown.
- else
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false
- - if @ci_commit
+ - if @pipeline
#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 290753d57c6..873ed9b59ee 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,7 +1,6 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
-- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
- if diff_view == 'parallel'
- fluid_layout true
@@ -15,16 +14,14 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
- = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do
- = icon('cloud-download fw')
+ = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
- %span.dropdown
+ %span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
- = icon('download')
Download as
%span.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
@@ -40,22 +37,22 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default
+ .light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- - if @commits.present?
+ - if @commits_count.nonzero?
%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
- %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count
+ %span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
- %span.badge= @commits.size
- - if @ci_commit
+ %span.badge= @commits_count
+ - if @pipeline
%li.builds-tab
= link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do
Builds
@@ -68,7 +65,7 @@
.tab-content
#notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block
- = render 'votes/votes_block', votable: @merge_request
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row
%section.col-md-12
diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml
index ba8d9a5835c..a60c445aa51 100644
--- a/app/views/projects/merge_requests/dropdowns/_branch.html.haml
+++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml
@@ -1,5 +1,5 @@
%ul
- branches.each do |branch|
%li
- %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } }
+ %a{ href: '#', class: "#{('is-active' if selected == branch)}", title: branch, data: { id: branch } }
= branch
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index b31ea5e5321..03159f123f3 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
-= render "header_title"
%h3.page-title
Edit Merge Request #{@merge_request.to_reference}
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index e56a44e0a79..ace275c689b 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,20 +1,20 @@
+- @no_container = true
- page_title "Merge Requests"
-= render "header_title"
-
+= render "projects/issues/head"
= render 'projects/last_push'
-.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)
+%div{ class: container_class }
+ .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)
- - 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
+ - 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
+ New Merge Request
-= render 'shared/issuable/filter', type: :merge_requests
+ = render 'shared/issuable/filter', type: :merge_requests
-.merge-requests-holder
- = render 'merge_requests'
+ .merge-requests-holder
+ = render 'merge_requests'
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index f5bf16ef3ad..a00d3128ffe 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,5 +1,4 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
-= render "header_title"
.merge-request
= render "projects/merge_requests/show/mr_title"
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index 92ce479d463..84b6c9ebc5c 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -5,6 +5,9 @@
- when :merge_when_build_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}");
+- when :sha_mismatch
+ :plain
+ $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
- else
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml
index d259968030e..2e798ce780a 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Merge Request"
-= render "header_title"
- if @merge_request.can_be_created && !params[:change_branches]
= render 'new_submit'
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
index a116ffe2e15..81de60f116c 100644
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -1,2 +1,2 @@
-= render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true
+= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a8f09f855d4..0b05785430b 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -2,4 +2,5 @@
= icon("sort-amount-desc")
Most recent commits displayed first
-= render "projects/commits/commits", project: @merge_request.project
+%ol#commits-list.list-unstyled
+ = render "projects/commits/commits", project: @merge_request.project
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 0dbd159298e..b3bea900d42 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
@@ -8,7 +8,7 @@
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button(clipboard_target: 'pre#merge-info-1')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button(clipboard_target: 'pre#merge-info-3')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button(clipboard_target: 'pre#merge-info-4')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
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 a23bd8d18d0..ebf18f6ac85 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,13 +1,13 @@
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@merge_request.title), pipeline: :single_line
+ = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
%div
- if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@merge_request.description, cache_key: [@merge_request, "description"])
+ = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
%textarea.hidden.js-task-list-field
= @merge_request.description
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 36c275e8be1..5bf5210aeab 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -25,8 +25,7 @@
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
- = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do
- = icon('pencil-square-o')
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
+ = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do
Edit
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 4d381754610..489c632ae22 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,15 +1,15 @@
-- if @ci_commit
+- if @pipeline
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) }
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status)
%span
CI build
= ci_label_for_status(status)
for
- - commit = @merge_request.last_commit
+ - commit = @merge_request.diff_head_commit
= succeed "." do
- = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace"
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
= link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
@@ -24,7 +24,7 @@
CI build
= ci_label_for_status(status)
for
- - commit = @merge_request.last_commit
+ - commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
%span.ci-coverage
@@ -33,12 +33,12 @@
.ci_widget
= icon("spinner spin")
- Checking CI status for #{@merge_request.last_commit_short_sha}&hellip;
+ Checking CI status for #{@merge_request.diff_head_commit.short_id}&hellip;
.ci_widget.ci-not_found{style: "display:none"}
= icon("times-circle")
- Could not find CI status for #{@merge_request.last_commit_short_sha}.
+ Could not find CI status for #{@merge_request.diff_head_commit.short_id}.
.ci_widget.ci-error{style: "display:none"}
= icon("times-circle")
- Could not connect to the CI server. Please check your settings and try again. \ No newline at end of file
+ Could not connect to the CI server. Please check your settings and try again.
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index ec4beae9727..19b5d0ff066 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -6,46 +6,29 @@
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- %div
- - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ %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"}.
- 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.
- = 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}'.
-
- .remove_source_branch_in_progress.hide
- %p
- = icon('spinner spin')
- Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
-
- :javascript
- $('.remove_source_branch').on('click', function() {
- $('.remove_source_branch_widget').hide();
- $('.remove_source_branch_in_progress').show();
- });
-
- $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) {
- location.reload();
- });
+ You can remove the source branch now.
+ = 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}'.
- $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) {
- $('.remove_source_branch_widget').hide();
- $('.remove_source_branch_in_progress').hide();
- $('.remove_source_branch_widget.failed').show();
- });
- - else
+ .remove_source_branch_in_progress.hide
%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"}.
- = render 'projects/merge_requests/widget/merged_buttons'
+ = icon('spinner spin')
+ Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
+ - else
+ %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"}.
+ = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 56167509af9..d836a253507 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -3,9 +3,9 @@
- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked?
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
- .btn-group
+ .clearfix.merged-buttons
- if can_remove_source_branch
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 55dbae598d3..0e0af57d76e 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -17,6 +17,8 @@
= render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
+ - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
+ = render 'projects/merge_requests/widget/open/build_failed'
- elsif @merge_request.can_be_merged?
= render 'projects/merge_requests/widget/open/accept'
@@ -26,4 +28,4 @@
%i.fa.fa-check
Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)}
= succeed '.' do
- != markdown issues_sentence(@closes_issues), pipeline: :gfm
+ != markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 3c68d61c4b5..d9efe81701f 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -13,7 +13,7 @@
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
- ci_status: "",
+ ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
@@ -26,4 +26,10 @@
builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
+ if (typeof merge_request_widget !== 'undefined') {
+ clearInterval(merge_request_widget.fetchBuildStatusInterval);
+ merge_request_widget.cancelPolling();
+ merge_request_widget.clearEventListeners();
+ }
+
merge_request_widget = new MergeRequestWidget(opts);
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 807833741af..bf2e76f0083 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,31 +1,36 @@
-- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
+- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
+ = hidden_field_tag :sha, @merge_request.diff_head_sha
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
.accept-action
- - if @ci_commit && @ci_commit.active?
+ - if @pipeline && @pipeline.active?
%span.btn-group
= button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
Merge When Build Succeeds
- = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
- %span.caret
- %span.sr-only
- Select Merge Moment
- %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %li
- = link_to "#", class: "merge_when_build_succeeds" do
- = icon('check fw')
- Merge When Build Succeeds
- %li
- = link_to "#", class: "accept_merge_request" do
- = icon('warning fw')
- Merge Immediately
+ - unless @project.only_allow_merge_if_build_succeeds?
+ = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
+ %span.caret
+ %span.sr-only
+ Select Merge Moment
+ %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
+ %li
+ = link_to "#", class: "merge_when_build_succeeds" do
+ = icon('check fw')
+ Merge When Build Succeeds
+ %li
+ = link_to "#", class: "accept_merge_request" do
+ = icon('warning fw')
+ Merge Immediately
- else
= f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do
Accept Merge Request
- - if @merge_request.can_remove_source_branch?(current_user)
+ - if @merge_request.force_remove_source_branch?
+ .accept-control
+ The source branch will be removed.
+ - elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
= check_box_tag :should_remove_source_branch
diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
new file mode 100644
index 00000000000..14f51af5360
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -0,0 +1,6 @@
+%h4
+ = icon('exclamation-triangle')
+ The build for this merge request failed
+
+%p
+ Please retry the build or push a new commit to fix the failure.
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index e6c089fefb2..06ab0a3fa00 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -1,9 +1,9 @@
-%h4
+%h4.has-conflicts
= icon("exclamation-triangle")
This merge request contains merge conflicts
%p
- Please resolve these conflicts or
+ Please resolve these conflicts or
- if @merge_request.can_be_merged_by?(current_user)
#{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
- else
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index 2168294c683..2b6b5e05e86 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -2,22 +2,21 @@
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the build succeeds.
%div
- - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present?
%p
= succeed '.' do
The changes will be merged into
%span.label-branch= @merge_request.target_branch
- - if should_remove_source_branch
+ - if @merge_request.remove_source_branch?
The source branch will be removed.
- else
The source branch will not be removed.
- - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user
+ - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
- user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
- = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
+ = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove Source Branch When Merged
diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
index a8145558ca8..57ce1959021 100644
--- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
@@ -1,4 +1,6 @@
-%h4
+%h4
Ready to be merged automatically
%p
Ask someone with write access to this repository to merge this request.
+ - if @merge_request.force_remove_source_branch?
+ The source branch will be removed.
diff --git a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml
new file mode 100644
index 00000000000..499624f8dd8
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml
@@ -0,0 +1,6 @@
+%h4
+ = icon("exclamation-triangle")
+ This merge request has received new commits since the page was loaded.
+
+%p
+ Please reload the page to review the new commits before merging.
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 687222fa92f..cbf1ba04170 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -17,9 +17,9 @@
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
- .col-sm-10= f.hidden_field :due_date
.col-sm-10
- .datepicker
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
.form-actions
- if @milestone.new_record?
@@ -28,10 +28,3 @@
-else
= f.submit 'Save changes', class: "btn-save btn"
= link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel"
-
-
-:javascript
- $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
- }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/projects/milestones/_header_title.html.haml b/app/views/projects/milestones/_header_title.html.haml
deleted file mode 100644
index 5f4b6982a6d..00000000000
--- a/app/views/projects/milestones/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Milestones", namespace_project_milestones_path(@project.namespace, @project))
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 43f8863163d..be682226ab6 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", @milestone.title, "Milestones"
-= render "header_title"
%h3.page-title
Edit Milestone ##{@milestone.iid}
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index abe567af1dd..ad2bfbec915 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,22 +1,22 @@
+- @no_container = true
- page_title "Milestones"
-= render "header_title"
+= render "projects/issues/head"
+%div{ class: container_class }
+ .top-area
+ = render 'shared/milestones_filter'
-.top-area
- = render 'shared/milestones_filter'
+ .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
+ New Milestone
- .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
+ = render @milestones
-.milestones
- %ul.content-list
- = render @milestones
+ - if @milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
- - if @milestones.blank?
- %li
- .nothing-here-block No milestones to show
-
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 0d016f78313..7f372b41698 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Milestone"
-= render "header_title"
%h3.page-title
New Milestone
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 6ec84660157..73772cc0e32 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,14 +1,12 @@
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
-= render "header_title"
-
.detail-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
Closed
- elsif @milestone.expired?
- Expired
+ Past due
- else
Open
%span.identifier
@@ -25,11 +23,9 @@
= 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 edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
- = icon('pencil-square-o')
Edit
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
- = icon('trash-o')
Delete
.detail-page-description.milestone-detail
diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml
index c609c505def..8f6805268d5 100644
--- a/app/views/projects/network/_head.html.haml
+++ b/app/views/projects/network/_head.html.haml
@@ -1,6 +1,9 @@
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render partial: 'shared/ref_switcher', locals: {destination: 'graph'}
+- @no_container = true
- .oneline
- You can move around the graph by using the arrow keys.
+%div{ class: container_class }
+ .row-content-block.second-block.content-component-block
+ .tree-ref-holder
+ = render partial: 'shared/ref_switcher', locals: {destination: 'graph'}
+
+ .oneline
+ You can move around the graph by using the arrow keys.
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 8065663ca2a..091af4df4a1 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,27 +1,21 @@
- page_title "Network", @ref
-= render "projects/commits/header_title"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/raphael.js')
+ = page_specific_javascript_tag('network/application.js')
= render "projects/commits/head"
= render "head"
-.project-network
- .controls
- = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f|
- = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha'
- = button_tag class: 'btn btn-success' do
- = icon('search')
- .inline.prepend-left-20
- .checkbox.light
- = label_tag :filter_ref do
- = check_box_tag :filter_ref, 1, @options[:filter_ref]
- %span Begin with the selected commit
+%div{ class: container_class }
+ .project-network
+ .controls
+ = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f|
+ = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha'
+ = button_tag class: 'btn btn-success' do
+ = icon('search')
+ .inline.prepend-left-20
+ .checkbox.light
+ = label_tag :filter_ref do
+ = check_box_tag :filter_ref, 1, @options[:filter_ref]
+ %span Begin with the selected commit
- .network-graph
- = spinner nil, true
-
-:javascript
- network_graph = new Network({
- url: "#{escape_javascript(@url)}",
- commit_url: "#{escape_javascript(@commit_url)}",
- ref: "#{escape_javascript(@ref)}",
- commit_id: '#{@commit.id}'
- })
- new ShortcutsNetwork(network_graph.branch_graph)
+ .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
+ = spinner nil, true
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index a4c6094c69a..05f33b78a47 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,109 +1,115 @@
- page_title 'New Project'
-- header_title "Projects", root_path
-
-%h3.page-title
- New Project
-%hr
+- header_title "Projects", dashboard_projects_path
.project-edit-container
.project-edit-errors
= render 'projects/errors'
- .project-edit-content
-
- = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f|
- .form-group.project-name-holder
- = f.label :path, class: 'control-label' do
- Project path
- .col-sm-10
- .input-group
- - if current_user.can_select_namespace?
- .input-group-addon
- = root_url
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- .input-group-addon
- \/
- - else
- .input-group-addon
- #{root_url}#{current_user.username}/
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
-
- - if current_user.can_create_group?
- .help-block
- Want to house several dependent projects under the same namespace?
- = link_to "Create a group", new_group_path
-
- - if import_sources_enabled?
- .project-import.js-toggle-container
- .form-group
- %label.control-label Import project from
- .col-sm-10
- - if github_import_enabled?
- - if github_import_configured?
- = link_to status_import_github_path, class: 'btn import_github' do
- %i.fa.fa-github
- GitHub
- - else
- = link_to '#', class: 'how_to_import_link btn import_github' do
- %i.fa.fa-github
- GitHub
- = render 'github_import_modal'
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ New project
+ %p
+ Create or Import your project from popular Git services
+ .col-lg-9
+ = form_for @project, html: { class: 'new_project' } do |f|
+ %fieldset.append-bottom-0
+ .form-group.col-xs-12.col-sm-6
+ = f.label :namespace_id, class: 'label-light' do
+ %span
+ Project path
+ .form-group
+ .input-group
+ - if current_user.can_select_namespace?
+ .input-group-addon
+ = root_url
+ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- - if bitbucket_import_enabled?
- - if bitbucket_import_configured?
- = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do
- %i.fa.fa-bitbucket
- Bitbucket
- else
- = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
- %i.fa.fa-bitbucket
- Bitbucket
- = render 'bitbucket_import_modal'
-
- - if gitlab_import_enabled?
- - if gitlab_import_configured?
- = link_to status_import_gitlab_path, class: 'btn import_gitlab' do
- %i.fa.fa-heart
- GitLab.com
- - else
- = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
- %i.fa.fa-heart
- GitLab.com
- = render 'gitlab_import_modal'
-
- - if gitorious_import_enabled?
- = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
- %i.icon-gitorious.icon-gitorious-small
- Gitorious.org
-
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- %i.fa.fa-google
- Google Code
-
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- %i.fa.fa-bug
- Fogbugz
-
- - if git_import_enabled?
- = link_to "#", class: 'btn js-toggle-button import_git' do
- %i.fa.fa-git
- %span Any repo by URL
-
- .js-toggle-content.hide
- = render "shared/import_form", f: f
-
- .prepend-botton-10
-
- .form-group
- = f.label :description, class: 'control-label' do
- Description
- %span.light (optional)
- .col-sm-10
- = f.text_area :description, class: "form-control", rows: 3, maxlength: 250, tabindex: 3
- = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
+ .input-group-addon.static-namespace
+ #{root_url}#{current_user.username}/
+ .form-group.col-xs-12.col-sm-6.project-path
+ = f.label :namespace_id, class: 'label-light' do
+ %span
+ Project name
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
+ - if current_user.can_create_group?
+ .help-block
+ Want to house several dependent projects under the same namespace?
+ = link_to "Create a group", new_group_path
+
+ - if import_sources_enabled?
+ .project-import.js-toggle-container
+ .form-group.clearfix
+ = f.label :visibility_level, class: 'label-light' do
+ Import project from
+ .col-sm-12.import-buttons
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn import_github' do
+ = icon 'github', text: 'GitHub'
+ %div
+ - if bitbucket_import_enabled?
+ - if bitbucket_import_configured?
+ = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do
+ %i.fa.fa-bitbucket
+ Bitbucket
+ - else
+ = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
+ %i.fa.fa-bitbucket
+ Bitbucket
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ - if gitlab_import_configured?
+ = link_to status_import_gitlab_path, class: 'btn import_gitlab' do
+ %i.fa.fa-heart
+ GitLab.com
+ - else
+ = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
+ %i.fa.fa-heart
+ GitLab.com
+ = render 'gitlab_import_modal'
+ %div
+ - if gitorious_import_enabled?
+ = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
+ %i.icon-gitorious.icon-gitorious-small
+ Gitorious.org
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ %i.fa.fa-google
+ Google Code
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ %i.fa.fa-bug
+ Fogbugz
+ %div
+ - if git_import_enabled?
+ = link_to "#", class: 'btn js-toggle-button import_git' do
+ %i.fa.fa-git
+ %span Repo by URL
+ %div
+ - if gitlab_project_import_enabled?
+ = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do
+ %i.fa.fa-gitlab
+ %span GitLab export
+
+ .js-toggle-content.hide
+ = render "shared/import_form", f: f
+
+ .form-group
+ = f.label :description, class: 'label-light' do
+ Project description
+ %span.light (optional)
+ = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
+
+ .form-group.project-visibility-level-holder
+ = f.label :visibility_level, class: 'label-light' do
+ Visibility Level
+ = link_to "(?)", help_page_path("public_access", "public_access")
+ = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
- .form-actions
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
@@ -119,6 +125,33 @@
e.preventDefault();
var import_modal = $(this).next(".modal").show();
});
+
$('.modal-header .close').bind('click', function() {
$(".modal").hide();
});
+
+ $('.import_gitlab_project').bind('click', function() {
+ var _href = $("a.import_gitlab_project").attr("href");
+ $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
+ });
+
+ $('.import_gitlab_project').attr('disabled',true)
+ $('.import_gitlab_project').attr('title', 'Project path required.');
+
+ $('.import_gitlab_project').click(function( event ) {
+ if($('.import_gitlab_project').attr('disabled')) {
+ event.preventDefault();
+ new Flash("Please enter a path for the project to be imported to.");
+ }
+ });
+
+ $('#project_path').keyup(function(){
+ if($(this).val().length !=0) {
+ $('.import_gitlab_project').attr('disabled', false);
+ $('.import_gitlab_project').attr('title','');
+ $(".flash-container").html("")
+ } else {
+ $('.import_gitlab_project').attr('disabled',true);
+ $('.import_gitlab_project').attr('title', 'Project path required.');
+ }
+ })
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 39be072855a..ec6c4938efc 100644
--- a/app/views/projects/notes/_diff_notes_with_reply.html.haml
+++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml
@@ -1,10 +1,7 @@
-- note = notes.first # example note
--# Check if line want not changed since comment was left
-- if !defined?(line) || line == note.diff_line
- %tr.notes_holder
- %td.notes_line{ colspan: 2 }
- %td.notes_content
- %ul.notes{ data: { discussion_id: note.discussion_id } }
- = render notes
- .discussion-reply-holder
- = link_to_reply_diff(note)
+- note = notes.first
+%tr.notes_holder
+ %td.notes_line{ colspan: 2 }
+ %td.notes_content
+ %ul.notes{ data: { discussion_id: note.discussion_id } }
+ = render partial: "projects/notes/note", collection: notes, as: :note
+ = link_to_reply_discussion(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 f8aa5e2fa7d..e50a4f86d03 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
@@ -1,27 +1,25 @@
-- note1 = notes_left.present? ? notes_left.first : nil
-- note2 = notes_right.present? ? notes_right.first : nil
+- note_left = notes_left.present? ? notes_left.first : nil
+- note_right = notes_right.present? ? notes_right.first : nil
%tr.notes_holder
- - if note1
+ - if note_left
%td.notes_line.old
%td.notes_content.parallel.old
- %ul.notes{ data: { discussion_id: note1.discussion_id } }
- = render notes_left
+ %ul.notes{ data: { discussion_id: note_left.discussion_id } }
+ = render partial: "projects/notes/note", collection: notes_left, as: :note
- .discussion-reply-holder
- = link_to_reply_diff(note1, 'old')
+ = link_to_reply_discussion(note_left, 'old')
- else
%td.notes_line.old= ""
%td.notes_content.parallel.old= ""
- - if note2
+ - if note_right
%td.notes_line.new
%td.notes_content.parallel.new
- %ul.notes{ data: { discussion_id: note2.discussion_id } }
- = render notes_right
+ %ul.notes{ data: { discussion_id: note_right.discussion_id } }
+ = render partial: "projects/notes/note", collection: notes_right, as: :note
- .discussion-reply-holder
- = link_to_reply_diff(note2, 'new')
+ = link_to_reply_discussion(note_right, 'new')
- else
%td.notes_line.new= ""
%td.notes_content.parallel.new= ""
diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml
index 572b00a38c7..7869d6413d8 100644
--- a/app/views/projects/notes/_discussion.html.haml
+++ b/app/views/projects/notes/_discussion.html.haml
@@ -1,13 +1,46 @@
- note = discussion_notes.first
+- expanded = !note.diff_note? || note.active?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: "avatar s40"
+ = image_tag avatar_icon(note.author), class: "avatar s40"
.timeline-content
- - if note.for_merge_request?
- - (active_notes, outdated_notes) = discussion_notes.partition(&:active?)
- = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0
- = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0
- - else
- = render "projects/notes/discussions/commit", discussion_notes: discussion_notes
+ .discussion.js-toggle-container{ class: note.discussion_id }
+ .discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+
+ .inline.discussion-headline-light
+ = note.author.to_reference
+ started a discussion on
+
+ - if note.for_commit?
+ - commit = note.noteable
+ - if commit
+ commit
+ = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace'
+ - else
+ a deleted commit
+ - else
+ - if note.active?
+ = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
+ the diff
+ - else
+ an outdated diff
+
+ = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago")
+
+ .discussion-actions
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
+ - if expanded
+ = icon("chevron-up")
+ - else
+ = icon("chevron-down")
+
+ Toggle discussion
+
+ .discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
+ - if note.diff_note?
+ = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes
+ - else
+ = render "projects/notes/discussions/notes", discussion_notes: discussion_notes
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index c87a3fadf72..8620f492282 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,6 +6,6 @@
= render 'projects/notes/hints'
.note-form-actions.clearfix
- = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
+ = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index d0ac380f216..7c61ba750fe 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -6,6 +6,8 @@
= f.hidden_field :line_code
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
+ = f.hidden_field :type
+ = f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
@@ -13,7 +15,7 @@
.error-alert
.note-form-actions.clearfix
- = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 0b002043408..7d1cbc62e86 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -5,4 +5,4 @@
is supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+ Attach a file \ No newline at end of file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index aeb7c1d5ee4..af0046886fb 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,5 +1,8 @@
+- return unless note.author
+- return if note.cross_reference_not_visible_for?(current_user)
+
- note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
+%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
@@ -8,28 +11,33 @@
.note-header
= link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light
- = "#{note.author.to_reference}"
- - if !note.system
+ = note.author.to_reference
+ - unless note.system
commented
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
- - access = note.project.team.human_max_access(note.author.id)
- - if access
- %span.note-role
- = access
+ - access = note_max_access_for_user(note)
+ - if access and not note.system
+ %span.note-role.hidden-xs= access
+ - if current_user and not note.system
+ = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
+ = icon('spinner spin')
+ = icon('smile-o')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
= icon('trash-o')
.note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text
= preserve do
- = markdown(note.note, pipeline: :note, cache_key: [note, "note"])
+ = note.note_html
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ .note-awards
+ = render 'award_emoji/awards_block', awardable: note, inline: false
- if note.attachment.url
.note-attachment
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index 62db86fb181..ebf7e8a9cb3 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -2,14 +2,9 @@
- @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
+ = render partial: "projects/notes/note", object: note, as: :note
- 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
+ = render partial: "projects/notes/note", object: note, as: :note
diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml
deleted file mode 100644
index 0ea8862a684..00000000000
--- a/app/views/projects/notes/discussions/_active.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- note = discussion_notes.first
-.discussion.js-toggle-container{ class: note.discussion_id }
- .discussion-header
- = link_to_member(@project, note.author, avatar: false)
- .inline.discussion-headline-light
- = "#{note.author.to_reference} started a discussion"
- = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
- on the diff
- = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
- .discussion-actions
- = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
- %i.fa.fa-chevron-up
- Show/hide discussion
-
- .discussion-body.js-toggle-content
- = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
deleted file mode 100644
index 2a2ead58eeb..00000000000
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- note = discussion_notes.first
-- commit = note.noteable
-- commit_description = commit ? 'commit' : 'a deleted commit'
-.discussion.js-toggle-container{ class: note.discussion_id }
- .discussion-header
- = link_to_member(@project, note.author, avatar: false)
- .inline.discussion-headline-light
- = "#{note.author.to_reference} started a discussion on #{commit_description}"
- - if commit
- = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
- = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
- .discussion-actions
- = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
- %i.fa.fa-chevron-up
- Show/hide discussion
- .discussion-body.js-toggle-content
- - if note.for_diff_line?
- = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
- - else
- .panel.panel-default
- .notes{ data: { discussion_id: discussion_notes.first.discussion_id } }
- %ul.notes.timeline
- = 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
deleted file mode 100644
index d46aab000c3..00000000000
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- diff = note.diff
-- if diff
- .diff-file
- .diff-header
- %span
- - if diff.deleted_file
- = diff.old_path
- - else
- = 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.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.diff-line-num= "..."
- %td.new_line.diff-line-num= "..."
- %td.line_content.match= line.text
- - else
- %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? "&nbsp;".html_safe : line.old_pos } }
- %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? "&nbsp;".html_safe : line.new_pos } }
- %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type)
-
- - if line_code == note.line_code
- = render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml
new file mode 100644
index 00000000000..4a69b8f8840
--- /dev/null
+++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml
@@ -0,0 +1,17 @@
+- note = discussion_notes.first
+- diff_file = note.diff_file
+- return unless diff_file
+
+- blob = note.blob
+
+.diff-file.file-holder
+ .file-title
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note)
+
+ .diff-content.code.js-syntax-highlight
+ %table
+ - note.truncated_diff_lines.each do |line|
+ = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
+
+ - if note.for_line?(line)
+ = render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml
new file mode 100644
index 00000000000..a785149549d
--- /dev/null
+++ b/app/views/projects/notes/discussions/_notes.html.haml
@@ -0,0 +1,6 @@
+- note = discussion_notes.first
+.panel.panel-default
+ .notes{ data: { discussion_id: note.discussion_id } }
+ %ul.notes.timeline
+ = render partial: "projects/notes/note", collection: discussion_notes, as: :note
+ = link_to_reply_discussion(note)
diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml
deleted file mode 100644
index 45141bcd1df..00000000000
--- a/app/views/projects/notes/discussions/_outdated.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- note = discussion_notes.first
-.discussion.js-toggle-container{ class: note.discussion_id }
- .discussion-header
- = link_to_member(@project, note.author, avatar: false)
- .inline.discussion-headline-light
- = "#{note.author.to_reference} started a discussion"
- on the outdated diff
- = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
- .discussion-actions
- = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
- %i.fa.fa-chevron-down
- Show/hide discussion
- .discussion-body.js-toggle-content.hide
- = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
new file mode 100644
index 00000000000..d65faf86d4e
--- /dev/null
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -0,0 +1,19 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
+
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
+
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
new file mode 100644
index 00000000000..8289aefcde7
--- /dev/null
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -0,0 +1,37 @@
+%p
+.commit-info-row
+ Pipeline
+ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
+ with
+ = pluralize @pipeline.statuses.count(:id), "build"
+ - if @pipeline.ref
+ for
+ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words @pipeline.duration
+
+ .pull-right
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
+ = ci_icon_for_status(@pipeline.status)
+ = ci_label_for_status(@pipeline.status)
+
+- if @commit
+ .commit-info-row
+ %span.light Authored by
+ %strong
+ = commit_author_link(@commit, avatar: true, size: 24)
+ #{time_ago_with_tooltip(@commit.authored_date)}
+
+.commit-info-row
+ %span.light Commit
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
+ = clipboard_button(clipboard_text: @pipeline.sha)
+
+- if @commit
+ .commit-box.content-block
+ %h3.commit-title
+ = markdown escape_once(@commit.title), pipeline: :single_line
+ - if @commit.description.present?
+ %pre.commit-description
+ = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
new file mode 100644
index 00000000000..6a127afa410
--- /dev/null
+++ b/app/views/projects/pipelines/index.html.haml
@@ -0,0 +1,58 @@
+- @no_container = true
+- page_title "Pipelines"
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ .top-area
+ %ul.nav-links
+ %li{class: ('active' if @scope.nil?)}
+ = link_to project_pipelines_path(@project) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(@pipelines_count)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to project_pipelines_path(@project, scope: :running) do
+ Running
+ %span.badge.js-running-count
+ = number_with_delimiter(@running_or_pending_count)
+
+ %li{class: ('active' if @scope == 'branches')}
+ = link_to project_pipelines_path(@project, scope: :branches) do
+ Branches
+
+ %li{class: ('active' if @scope == 'tags')}
+ = link_to project_pipelines_path(@project, scope: :tags) do
+ Tags
+
+ .nav-controls
+ - if can? current_user, :create_pipeline, @project
+ = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
+ New pipeline
+
+ - unless @repository.gitlab_ci_yml
+ = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
+ = link_to ci_lint_path, class: 'btn btn-default' do
+ %span CI Lint
+
+ %ul.content-list.pipelines
+ - stages = @pipelines.stages
+ - if @pipelines.blank?
+ %li
+ .nothing-here-block No pipelines to show
+ - else
+ .table-holder
+ %table.table.builds
+ %tbody
+ %th ID
+ %th Commit
+ - stages.each do |stage|
+ %th.stage
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ %th Duration
+ %th
+ = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
+
+ = paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
new file mode 100644
index 00000000000..5f4ec2e40c8
--- /dev/null
+++ b/app/views/projects/pipelines/new.html.haml
@@ -0,0 +1,21 @@
+- page_title "New Pipeline"
+
+%h3.page-title
+ New Pipeline
+%hr
+
+= form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
+ = form_errors(@pipeline)
+ .form-group
+ = f.label :ref, 'Create for', class: 'control-label'
+ .col-sm-10
+ = f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
+ .help-block Existing branch name, tag
+ .form-actions
+ = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
+ = link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel'
+
+:javascript
+ var availableRefs = #{@project.repository.ref_names.to_json};
+
+ new NewBranchForm($('.js-new-pipeline-form'), availableRefs)
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
new file mode 100644
index 00000000000..75943c64276
--- /dev/null
+++ b/app/views/projects/pipelines/show.html.haml
@@ -0,0 +1,8 @@
+- page_title "Pipeline"
+
+.prepend-top-default
+ - if @commit
+ = render "projects/pipelines/info"
+ %div.block-connector
+
+= render "projects/commit/pipeline", pipeline: @pipeline
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index c53033e367c..e783d8c72c5 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -2,16 +2,17 @@
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{members.count})
+ %span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
- = link_to group_group_members_path(@group), class: 'btn' do
- = icon('pencil-square-o')
- Manage group members
+ = link_to 'Manage group members',
+ group_group_members_path(@group),
+ class: 'btn'
%ul.content-list
- - members.limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
- - if members.count > 20
+ = render partial: 'shared/members/member',
+ collection: members.limit(20),
+ as: :member,
+ locals: { show_controls: false }
+ - if members.size > 20
%li
and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
diff --git a/app/views/projects/project_members/_header_title.html.haml b/app/views/projects/project_members/_header_title.html.haml
deleted file mode 100644
index a31f0a37fa2..00000000000
--- a/app/views/projects/project_members/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Members", namespace_project_project_members_path(@project.namespace, @project))
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 f0f3bb3c177..82892a33358 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
.col-sm-10
- = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
deleted file mode 100644
index 05bf3a7ef6a..00000000000
--- a/app/views/projects/project_members/_project_member.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-
-%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
- %span.list-item-name
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if can?(current_user, :admin_project_member, @project)
- = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
-
- - if can?(current_user, :admin_project_member, @project)
- .pull-right
- %strong= member.human_access
- - if can?(current_user, :update_project_member, member)
- = button_tag class: "btn-xs btn js-toggle-button",
- title: 'Edit access level', type: 'button' do
- %i.fa.fa-pencil-square-o
-
- - if can?(current_user, :destroy_project_member, member)
- &nbsp;
- - if current_user == user
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
- = icon("sign-out")
- Leave
- - else
- = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
- %i.fa.fa-minus.fa-inverse
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index ae13f8428f0..77370c14def 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -1,6 +1,7 @@
- @project_group_links.each do |group_links|
- shared_group = group_links.group
- - shared_group_users_count = group_links.group.group_members.count
+ - shared_group_members = shared_group.members
+ - shared_group_users_count = shared_group_members.size
.panel.panel-default
.panel-heading
Shared with
@@ -14,8 +15,10 @@
%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
+ = render partial: 'shared/members/member',
+ collection: shared_group_members.order(access_level: :desc).limit(20),
+ as: :member,
+ locals: { 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 e8dce30425f..b0bfdd235f7 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -2,8 +2,7 @@
.panel-heading
%strong #{@project.name}
project members
- %small
- (#{members.count})
+ %span.badge= members.size
.controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
@@ -11,8 +10,7 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - members.each do |project_member|
- = render 'project_member', member: project_member
+ = render partial: 'shared/members/member', collection: members, as: :member
:javascript
$('form.member-search-form').on('submit', function (event) {
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 189906498cb..eef97107d77 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -1,5 +1,4 @@
- page_title "Import members"
-= render "header_title"
%h3.page-title
Import members from another project
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index ebcfc907ebb..9031f01b496 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,5 +1,4 @@
- page_title "Members"
-= render "header_title"
.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
@@ -14,7 +13,9 @@
Users with access to this project are listed below.
= render "new_project_member"
- = render "team", members: @project_members
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+
+ = render 'team', members: @project_members
- if @group
= render "group_members", members: @group_members
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 2fb3a41d541..45f8ef89060 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}');
+ $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index b9e9dd8aaea..97cb1a9052b 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,7 +1,7 @@
%h5.prepend-top-0
- Already Protected (#{@branches.size})
-- if @branches.empty?
- %p.profile-settings-message.text-center
+ Already Protected (#{@protected_branches.size})
+- if @protected_branches.empty?
+ %p.settings-message.text-center
No branches are protected, protect a branch with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
@@ -9,33 +9,18 @@
%table.table.protected-branches-list
%colgroup
%col{ width: "30%" }
- %col{ width: "30%" }
+ %col{ width: "25%" }
%col{ width: "25%" }
- if can_admin_project
%col
%thead
%tr
- %th Branch
- %th Last commit
- %th Developers can push
+ %th Protected Branch
+ %th Commit
+ %th Developers Can Push
- if can_admin_project
%th
%tbody
- - @branches.each do |branch|
- - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch)
- %tr
- %td
- = link_to(branch.name, namespace_project_commits_path(@project.namespace, @project, branch.name))
- - if @project.root_ref?(branch.name)
- %span.label.label-info.prepend-left-5 default
- %td
- - if commit = branch.commit
- = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
- #{time_ago_with_tooltip(commit.committed_date)}
- - else
- (branch was removed from repository)
- %td
- = check_box_tag("developers_can_push", branch.id, branch.developers_can_push, data: { url: @url })
- - if can_admin_project
- %td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm"
+ = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
+
+ = paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
new file mode 100644
index 00000000000..b803d932e67
--- /dev/null
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -0,0 +1,17 @@
+= f.hidden_field(:name)
+
+= dropdown_tag("Protected Branch",
+ options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit',
+ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches",
+ footer_content: true,
+ data: { show_no: true, show_any: true, show_upcoming: true,
+ selected: params[:protected_branch_name],
+ project_id: @project.try(:id) } }) do
+
+ %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list
+ %li
+ = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do
+ Create new
+
+:javascript
+ new ProtectedBranchSelect();
diff --git a/app/views/projects/protected_branches/_matching_branch.html.haml b/app/views/projects/protected_branches/_matching_branch.html.haml
new file mode 100644
index 00000000000..8a5332ca5bb
--- /dev/null
+++ b/app/views/projects/protected_branches/_matching_branch.html.haml
@@ -0,0 +1,9 @@
+%tr
+ %td
+ = link_to matching_branch.name, namespace_project_tree_path(@project.namespace, @project, matching_branch.name)
+ - if @project.root_ref?(matching_branch.name)
+ %span.label.label-info.prepend-left-5 default
+ %td
+ - commit = @project.commit(matching_branch.name)
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
new file mode 100644
index 00000000000..474aec3a97c
--- /dev/null
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -0,0 +1,21 @@
+- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
+%tr
+ %td
+ = protected_branch.name
+ - if @project.root_ref?(protected_branch.name)
+ %span.label.label-info.prepend-left-5 default
+ %td
+ - if protected_branch.wildcard?
+ - matching_branches = protected_branch.matching(repository.branches)
+ = link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
+ - else
+ - if commit = protected_branch.commit
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = time_ago_with_tooltip(commit.committed_date)
+ - else
+ (branch was removed from repository)
+ %td
+ = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url })
+ - if can_admin_project
+ %td
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right"
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index c7d317dbaee..5669713d9a1 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -4,30 +4,38 @@
.col-lg-3
%h4.prepend-top-0
= page_title
- %p Keep stable branches secure and force developers to use Merge Requests
- .col-lg-9
- %h5.prepend-top-0
- Protect a branch
- .account-well.append-bottom-default
- %p.light-header.append-bottom-0 Protected branches are designed to
+ %p Keep stable branches secure and force developers to use merge requests.
+ %p.prepend-top-20
+ Protected branches are designed to:
%ul
%li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"}
%li prevent anyone from force pushing to the branch
%li prevent anyone from deleting the branch
%p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"}
+ .col-lg-9
+ %h5.prepend-top-0
+ Protect a branch
- if can? current_user, :admin_project, @project
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
= form_errors(@protected_branch)
.form-group
= f.label :name, "Branch", class: "label-light"
- = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}})
+ = render partial: "dropdown", locals: { f: f }
+ %p.help-block
+ = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches")
+ such as
+ %code *-stable
+ or
+ %code production/*
+ are supported.
+
.form-group
= f.check_box :developers_can_push, class: "pull-left"
.prepend-left-20
= f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Allow developers to push to this branch
- = f.submit "Protect", class: "btn-create btn"
+ = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true
%hr
= render "branches_list"
diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml
new file mode 100644
index 00000000000..4d8169815b3
--- /dev/null
+++ b/app/views/projects/protected_branches/show.html.haml
@@ -0,0 +1,25 @@
+- page_title @protected_branch.name, "Protected Branches"
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = @protected_branch.name
+
+ .col-lg-9
+ %h5 Matching Branches
+ - if @matching_branches.present?
+ .table-responsive
+ %table.table.protected-branches-list
+ %colgroup
+ %col{ width: "30%" }
+ %col{ width: "30%" }
+ %thead
+ %tr
+ %th Branch
+ %th Last commit
+ %tbody
+ - @matching_branches.each do |matching_branch|
+ = render partial: "matching_branch", object: matching_branch
+ - else
+ %p.settings-message.text-center
+ Couldn't find any matching branches.
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 0d59cec322c..835398b6f98 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", @tag.name, "Tags"
-= render "projects/commits/header_title"
= render "projects/commits/head"
.row-content-block
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 6ca919f7f80..43a6fdfd103 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
- = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line
+ = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
%td
%span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
new file mode 100644
index 00000000000..c45a9d4f81f
--- /dev/null
+++ b/app/views/projects/runners/_form.html.haml
@@ -0,0 +1,38 @@
+= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f|
+ = form_errors(runner)
+ .form-group
+ = label :active, "Active", class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :active
+ %span.light Paused runners don't accept new builds
+ .form-group
+ = label :run_untagged, 'Run untagged jobs', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :run_untagged
+ %span.light Indicates whether this runner can pick jobs without tags
+ .form-group
+ = label :locked, 'Lock to current projects', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :locked
+ %span.light When a runner is locked, it cannot be assigned to other projects
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control'
+ .help-block You can setup jobs to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 47ec420189d..85225857758 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -2,16 +2,18 @@
%h4
= runner_status_icon(runner)
%span.monospace
- - if @runners.include?(runner)
+ - if @project_runners.include?(runner)
= link_to runner.short_sha, runner_path(runner)
+ - if runner.locked?
+ = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
%small
- =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
+ = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn
- else
= runner.short_sha
.pull-right
- - if @runners.include?(runner)
+ - if @project_runners.include?(runner)
- if runner.belongs_to_one_project?
= link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 30cd1263a12..d469dda5b81 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -8,7 +8,7 @@
Install GitLab Runner software.
Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
%li
- Specify following URL during runner setup:
+ Specify the following URL during runner setup:
%code #{ci_root_url(only_path: false)}
%li
Use the following registration token during setup:
@@ -17,13 +17,13 @@
Start runner!
-- if @runners.any?
+- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @runners, as: :runner
+ = render partial: 'runner', collection: @project_runners, as: :runner
-- if @specific_runners.any?
+- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @specific_runners, as: :runner
- = paginate @specific_runners
+ = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = paginate @assignable_runners
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index eba03028af8..95706888655 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,29 +1,6 @@
- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
%h4 Runner ##{@runner.id}
+
%hr
-= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f|
- .form-group
- = label :active, "Active", class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :active
- %span.light Paused runners don't accept new builds
- .form-group
- = label_tag :token, class: 'control-label' do
- Token
- .col-sm-10
- = f.text_field :token, class: 'form-control', readonly: true
- .form-group
- = label_tag :description, class: 'control-label' do
- Description
- .col-sm-10
- = f.text_field :description, class: 'form-control'
- .form-group
- = label_tag :tag_list, class: 'control-label' do
- Tags
- .col-sm-10
- = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control'
- .help-block You can setup jobs to only use runners with specific tags
- .form-actions
- = f.submit 'Save changes', class: 'btn btn-save'
+ = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 5bf4c09ca25..61b99f35d74 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -17,50 +17,42 @@
%th Property Name
%th Value
%tr
- %td
- Tags
+ %td Active
+ %td= @runner.active? ? 'Yes' : 'No'
+ %tr
+ %td Can run untagged jobs
+ %td= @runner.run_untagged? ? 'Yes' : 'No'
+ %tr
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
+ %tr
+ %td Tags
%td
- @runner.tag_list.each do |tag|
%span.label.label-primary
= tag
%tr
- %td
- Name
- %td
- = @runner.name
+ %td Name
+ %td= @runner.name
%tr
- %td
- Version
- %td
- = @runner.version
+ %td Version
+ %td= @runner.version
%tr
- %td
- Revision
- %td
- = @runner.revision
+ %td Revision
+ %td= @runner.revision
%tr
- %td
- Platform
- %td
- = @runner.platform
+ %td Platform
+ %td= @runner.platform
%tr
- %td
- Architecture
- %td
- = @runner.architecture
+ %td Architecture
+ %td= @runner.architecture
%tr
- %td
- Description
- %td
- = @runner.description
+ %td Description
+ %td= @runner.description
%tr
- %td
- Last contact
+ %td Last contact
%td
- if @runner.contacted_at
#{time_ago_in_words(@runner.contacted_at)} ago
- else
Never
-
-
-
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 1b70880043a..1f13ea28b4e 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,18 +1,16 @@
-%h3.page-title
- = @service.title
- = boolean_to_icon @service.activated?
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = @service.title
+ = boolean_to_icon @service.activated?
-%p= @service.description
-
-%hr
-
-= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
- = render 'shared/service_settings', form: form
-
- .form-actions
- = form.submit 'Save changes', class: 'btn btn-save'
- &nbsp;
- - if @service.valid? && @service.activated?
- - disabled = @service.can_test? ? '':'disabled'
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}"
- = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
+ %p= @service.description
+ .col-lg-9
+ = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
+ = render 'shared/service_settings', form: form
+ = form.submit 'Save changes', class: 'btn btn-save'
+ &nbsp;
+ - if @service.valid? && @service.activated?
+ - disabled = @service.can_test? ? '':'disabled'
+ = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}"
+ = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index c1356f6db02..4a33a5bc6f6 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -1,24 +1,32 @@
- page_title "Services"
-%h3.page-title Project services
-%p.light Project services allow you to integrate GitLab with other applications
-.table-holder
- %table.table
- %thead
- %tr
- %th
- %th Service
- %th Description
- %th Last edit
- - @services.sort_by(&:title).each do |service|
- %tr
- %td
- = boolean_to_icon service.activated?
- %td
- = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
- %strong= service.title
- %td
- = service.description
- %td.light
- = time_ago_in_words service.updated_at
- ago
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Project services
+ %p Project services allow you to integrate GitLab with other applications
+ .col-lg-9
+ %table.table
+ %colgroup
+ %col
+ %col
+ %col.hidden-xs
+ %col{ width: "120" }
+ %thead
+ %tr
+ %th
+ %th Service
+ %th.hidden-xs Description
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = boolean_to_icon service.activated?
+ %td
+ = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
+ %strong= service.title
+ %td.hidden-xs
+ = service.description
+ %td.light
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 9b3d3f069d9..11310d5e1e1 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.id namespace_project_url(@project.namespace, @project)
xml.updated @events[0].updated_at.xmlschema if @events[0]
- @events.each do |event|
- event_to_atom(xml, event)
- end
+ xml << render(@events) if @events.any?
end
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 74feb9e3282..58d8e068754 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,21 +12,20 @@
= render 'projects/last_push'
= render "home_panel"
-.project-stats.row-content-block.second-block
+%nav.project-stats{ class: (container_class) }
%ul.nav
%li
+ = link_to project_files_path(@project) do
+ Files (#{repository_size})
+ %li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- = pluralize(number_with_delimiter(@project.commit_count), 'commit')
+ #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
- = pluralize(number_with_delimiter(@repository.branch_names.count), 'branch')
+ #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
- = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag')
-
- %li
- = link_to project_files_path(@project) do
- = repository_size
+ #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
%li
@@ -57,11 +56,23 @@
%li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
+ - unless @repository.gitlab_ci_yml
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+ Set Up CI
+ %li.project-repo-buttons-right
+ .project-repo-buttons.project-right-buttons
+ - if current_user
+ = render 'shared/members/access_request_buttons', source: @project
+
+ .btn-group.project-repo-btn-group
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
+ = render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
- .content-block.second-block.white
- %div{ class: container_class }
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ .project-last-commit{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 4a515469422..bf57beb9d07 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,11 +1,27 @@
-= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
- = icon('plus')
- New Snippet
-- if can?(current_user, :admin_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
- = icon('trash-o')
- Delete
-- if can?(current_user, :update_project_snippet, @snippet)
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
- = icon('pencil-square-o')
- Edit
+.hidden-xs
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
+ Delete
+.visible-xs-block.dropdown
+ %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
+ Options
+ %span.caret
+ .dropdown-menu.dropdown-menu-full-width
+ %ul
+ %li
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do
+ New Snippet
+ - if can?(current_user, :update_project_snippet, @snippet)
+ %li
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
+ Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ %li
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
+ Delete
diff --git a/app/views/projects/snippets/_header_title.html.haml b/app/views/projects/snippets/_header_title.html.haml
deleted file mode 100644
index 04f0bbe9853..00000000000
--- a/app/views/projects/snippets/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Snippets", namespace_project_snippets_path(@project.namespace, @project))
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index dc3ea1fcf12..216f70f5605 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,5 +1,4 @@
- page_title "Edit", @snippet.title, "Snippets"
-= render "header_title"
%h3.page-title
Edit Snippet
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 103ff447464..96fee3b17b2 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,5 +1,4 @@
- page_title "Snippets"
-= render "header_title"
.row-content-block.top-block
.pull-right
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index e57237991b4..772a594269c 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Snippets"
-= render "header_title"
%h3.page-title
New Snippet
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 7c599563ce4..bae4d8f349f 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,18 +1,15 @@
- page_title @snippet.title, "Snippets"
-= render "header_title"
.snippet-holder
= render 'shared/snippets/header'
- %article.file-holder
- .file-title
+ %article.file-holder.file-holder-no-border.snippet-file-content
+ .file-title.file-title-clear
= blob_icon 0, @snippet.file_name
- %strong
- = @snippet.file_name
+ = @snippet.file_name
.file-actions.hidden-xs
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
-
= render 'shared/snippets/blob'
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
index 093d1d1bb0f..8a11dbfa9f4 100644
--- a/app/views/projects/tags/_download.html.haml
+++ b/app/views/projects/tags/_download.html.haml
@@ -1,7 +1,6 @@
-%span.btn-group.btn-grouped
+%span.btn-group
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
- %i.fa.fa-download
- %span source code
+ %span Source code
%a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
%span.caret
%span.sr-only
@@ -9,9 +8,7 @@
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %i.fa.fa-download
%span Download zip
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %i.fa.fa-download
%span Download tar.gz
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index dbc35c16feb..2c11c0e5b21 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,11 +15,11 @@
= render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if commit
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
index ffeacb5a004..e4a78fadbeb 100644
--- a/app/views/projects/tags/destroy.js.haml
+++ b/app/views/projects/tags/destroy.js.haml
@@ -1,3 +1,2 @@
-$('.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/index.html.haml b/app/views/projects/tags/index.html.haml
index dc6ece30dd2..368231e73fe 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,29 +1,41 @@
+- @no_container = true
- page_title "Tags"
-= render "projects/commits/header_title"
= render "projects/commits/head"
-.row-content-block
- - if can? current_user, :push_code, @project
- .pull-right
- = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
- = icon('plus')
- New tag
- .oneline
- Tags give the ability to mark specific points in history as being important
+%div{ class: container_class }
+ .top-area
+ .nav-text
+ Tags give the ability to mark specific points in history as being important
-.tags
- - unless @tags.empty?
- %ul.content-list
- - @tags.each do |tag|
- = render 'tag', tag: @repository.find_tag(tag)
+ .nav-controls
+ - if can? current_user, :push_code, @project
+ = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
+ New tag
+ .dropdown.inline
+ %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
+ %span.light= @sort.humanize
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ = link_to namespace_project_tags_path(sort: nil) do
+ Name
+ = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
- = paginate @tags, theme: 'gitlab'
+ .tags
+ - if @tags.any?
+ %ul.content-list
+ = render partial: 'tag', collection: @tags
- - else
- .nothing-here-block
- Repository has no tags yet.
- %br
- %small
- Use git tag command to add a new one:
+ = paginate @tags, theme: 'gitlab'
+
+ - else
+ .nothing-here-block
+ Repository has no tags yet.
%br
- %span.monospace git tag -a v1.4 -m 'version 1.4'
+ %small
+ Use git tag command to add a new one:
+ %br
+ %span.monospace git tag -a v1.4 -m 'version 1.4'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index f9306453297..3a097750d6e 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,5 +1,4 @@
- page_title "New Tag"
-= render "projects/commits/header_title"
- if @error
.alert.alert-danger
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 9f1424aecc7..b7d7d5c5382 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -1,5 +1,4 @@
- page_title @tag.name, "Tags"
-= render "projects/commits/header_title"
= render "projects/commits/head"
.row-content-block
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 2ddc5d504fa..a3a4dba3fa4 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,8 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- %span.str-truncated
- = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
+ - file_name = blob_item.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
+ %span.str-truncated= file_name
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index cf65057e704..9577696fc0d 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -1,9 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- %span.str-truncated
- - path = flatten_tree(tree_item)
- = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
+ - path = flatten_tree(tree_item)
+ = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
+ %span.str-truncated= path
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 91fb2a44594..bf5360b4dee 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,17 +1,20 @@
+- @no_container = true
+
- page_title @path.presence || "Files", @ref
-- header_title project_title(@project, "Files", project_files_path(@project))
= 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'
+= render "projects/commits/head"
-.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
+%div{ class: container_class }
+ .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
- .nav-block
- = render 'projects/tree/tree_header', tree: @tree
+ #tree-holder.tree-holder.clearfix
+ .nav-block
+ = render 'projects/tree/tree_header', tree: @tree
- = render 'projects/tree/tree_content', tree: @tree
+ = render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f91885b216d..7f3de47d7df 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -5,7 +5,7 @@
%h4.prepend-top-0
= page_title
%p
- Triggers can be used to force a rebuild of a specific branch or tag with an API call.
+ Triggers can force a specific branch or tag to rebuild with an API call.
.col-lg-9
%h5.prepend-top-0
Your triggers
@@ -18,8 +18,8 @@
%th
= render partial: 'trigger', collection: @triggers, as: :trigger
- else
- %p.profile-settings-message.text-center.append-bottom-default
- There are no triggers to use, add one by the button below.
+ %p.settings-message.text-center.append-bottom-default
+ No triggers have been created yet. Add one using the button below.
= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
= f.submit "Add Trigger", class: 'btn btn-success'
@@ -28,8 +28,7 @@
Use CURL
%p.light
- Copy the token above and set your branch or tag name. This is the reference that will be rebuild.
-
+ Copy the token above, set your branch or tag name, and that reference will be rebuilt.
%pre
:plain
@@ -41,10 +40,10 @@
Use .gitlab-ci.yml
%p.light
- Copy the snippet to
- %i .gitlab-ci.yml
- of dependent project.
- At the end of your build it will trigger this project to rebuilt.
+ In the
+ %code .gitlab-ci.yml
+ of the dependent project, include the following snippet.
+ The project will rebuild at the end of the build.
%pre
:plain
@@ -57,9 +56,8 @@
%p.light
Add
- %strong variables[VARIABLE]=VALUE
- to API request.
- The value of variable could then be used to distinguish triggered build from normal one.
+ %code variables[VARIABLE]=VALUE
+ to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
%pre.append-bottom-0
:plain
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
new file mode 100644
index 00000000000..0249e0c1bf1
--- /dev/null
+++ b/app/views/projects/variables/_content.html.haml
@@ -0,0 +1,8 @@
+%h4.prepend-top-0
+ Secret Variables
+%p
+ These variables will be set to environment by the runner.
+%p
+ So you can use them for passwords, secret keys or whatever you want.
+%p
+ The value of the variable can be visible in build log if explicitly asked to do so.
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml
new file mode 100644
index 00000000000..a5bae83e0ce
--- /dev/null
+++ b/app/views/projects/variables/_form.html.haml
@@ -0,0 +1,10 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f|
+ = form_errors(@variable)
+
+ .form-group
+ = f.label :key, "Key", class: "label-light"
+ = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+ .form-group
+ = f.label :value, "Value", class: "label-light"
+ = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+ = f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
new file mode 100644
index 00000000000..6c43f822db4
--- /dev/null
+++ b/app/views/projects/variables/_table.html.haml
@@ -0,0 +1,25 @@
+.table-responsive.variables-table
+ %table.table
+ %colgroup
+ %col
+ %col
+ %col{ width: 100 }
+ %thead
+ %th Key
+ %th Value
+ %th
+ %tbody
+ - @project.variables.each do |variable|
+ - if variable.id?
+ %tr
+ %td= variable.key
+ %td= variable.value
+ %td
+ = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
+ %span.sr-only
+ Update
+ = icon("pencil")
+ = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
+ %span.sr-only
+ Remove
+ = icon("trash")
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml
new file mode 100644
index 00000000000..09bb54600af
--- /dev/null
+++ b/app/views/projects/variables/index.html.haml
@@ -0,0 +1,17 @@
+- page_title "Variables"
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "content"
+ .col-lg-9
+ %h5.prepend-top-0
+ Add a variable
+ = render "form", btn_text: "Add new variable"
+ %hr
+ %h5.prepend-top-0
+ Your variables (#{@project.variables.size})
+ - if @project.variables.empty?
+ %p.settings-message.text-center.append-bottom-0
+ No variables found, add one with the form above.
+ - else
+ = render "table"
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index ca284b84d39..297a53ca98c 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -1,36 +1,9 @@
- page_title "Variables"
-%h3.page-title
- Secret Variables
-%p.light
- 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
-
-
-= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
- = form_errors(@project)
-
- = f.fields_for :variables do |variable_form|
- .form-group
- = variable_form.label :key, 'Key', class: 'control-label'
- .col-sm-10
- = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE"
-
- .form-group
- = variable_form.label :value, 'Value', class: 'control-label'
- .col-sm-10
- = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: ""
-
- = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10'
- %hr
- %p
- .clearfix
- = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right'
-
- .form-actions
- = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "content"
+ .col-lg-9
+ %h5.prepend-top-0
+ Update variable
+ = render "form", btn_text: "Save variable"
diff --git a/app/views/projects/wikis/_header_title.html.haml b/app/views/projects/wikis/_header_title.html.haml
deleted file mode 100644
index 408adc36ca6..00000000000
--- a/app/views/projects/wikis/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, 'Wiki', get_project_wiki_path(@project))
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 2b91b7e8f65..4ea75dbbf0c 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,11 +1,12 @@
- if (@page && @page.persisted?)
- = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" 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
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
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 a722fbc5352..f8ea479e0b1 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,5 +1,5 @@
-.top-area
- %ul.nav-links
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
@@ -10,10 +10,4 @@
= 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'
+ = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 919daf0a7b2..c32cb122c26 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -1,14 +1,17 @@
-%div#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title New Wiki Page
- .modal-body
- %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'
+- @no_container = true
+
+%div{ class: container_class }
+ %div#modal-new-wiki.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title New Wiki Page
+ .modal-body
+ %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 4dd818c7f67..233538bb488 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,20 +1,25 @@
+- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
-= render "header_title"
= render 'nav'
-.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
+%div{ class: container_class }
+ .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'
+ .nav-controls
+ - if !(@page && @page.persisted?)
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ = render 'main_links'
-= render 'form'
+ = render 'form'
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index c7e490c3cd1..7dfa405d063 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,5 +1,4 @@
- page_title "Wiki"
-= render "header_title"
%h3.page-title Empty page
%hr
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index ba3f2cadc48..b8811a28dd6 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,33 +1,34 @@
+- @no_container = true
- page_title "Git Access", "Wiki"
-= render "header_title"
= render 'nav'
-.row-content-block
- %span.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+%div{ class: container_class }
+ .sub-header-block
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
-.git-empty.prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ .prepend-top-default
+ %fieldset
+ %legend Install Gollum:
+ %pre.dark
+ :preserve
+ gem install gollum
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %legend Clone Your Wiki:
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+ %legend Start Gollum And Edit Locally:
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index dcaddae2b04..4c0b14e2c42 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,38 +1,37 @@
- page_title "History", @page.title.capitalize, "Wiki"
-= render "header_title"
= render 'nav'
+%div{ class: container_class }
+ .top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
-.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
- %thead
- %tr
- %th Page version
- %th Author
- %th Commit Message
- %th Last updated
- %th Format
- %tbody
- - @page.versions.each_with_index do |version, index|
- - commit = version
+ .table-holder
+ %table.table
+ %thead
%tr
- %td
- = link_to project_wiki_path_with_version(@project, @page,
- commit.id, index == 0) do
- = truncate_sha(commit.id)
- %td
- = commit.author.name
- %td
- = commit.message
- %td
- #{time_ago_with_tooltip(version.authored_date)}
- %td
- %strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ %th Page version
+ %th Author
+ %th Commit Message
+ %th Last updated
+ %th Format
+ %tbody
+ - @page.versions.each_with_index do |version, index|
+ - commit = version
+ %tr
+ %td
+ = link_to project_wiki_path_with_version(@project, @page,
+ commit.id, index == 0) do
+ = truncate_sha(commit.id)
+ %td
+ = commit.author.name
+ %td
+ = commit.message
+ %td
+ #{time_ago_with_tooltip(version.authored_date)}
+ %td
+ %strong
+ = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 92b494a513c..9c10acd4cb6 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,13 +1,14 @@
+- @no_container = true
- page_title "Pages", "Wiki"
-= render "header_title"
= render 'nav'
-%ul.content-list
- - @wiki_pages.each do |wiki_page|
- %li
- = 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'
+%div{ class: container_class }
+ %ul.content-list
+ - @wiki_pages.each do |wiki_page|
+ %li
+ = 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 067fb7f8f54..5cebb538cf5 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,25 +1,26 @@
+- @no_container = true
- page_title @page.title.capitalize, "Wiki"
-= render "header_title"
= render 'nav'
-.top-area
- .nav-text
- %strong= @page.title.capitalize
+%div{ class: container_class }
+ .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)}
+ %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'
+ .nav-controls
+ = render 'main_links'
-- if @page.historical?
- .warning_message
- This is an old version of this page.
- You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
+ - if @page.historical?
+ .warning_message
+ This is an old version of this page.
+ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-.wiki-holder.prepend-top-default
- .wiki
- = preserve do
- = render_wiki_content(@page)
+ .wiki-holder.prepend-top-default.append-bottom-default
+ .wiki
+ = preserve do
+ = render_wiki_content(@page)
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 0fe8a3b490a..290743feb4a 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -2,9 +2,10 @@
.blob-result
.file-holder
.file-title
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do
+ - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
+ = link_to blob_link do
%i.fa.fa-file
%strong
= blob.filename
.file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 640890fbe92..8f68d6d1b87 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -7,7 +7,7 @@
- if issue.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project }))
+ = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 333f6533213..6331c2bd6b0 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, { project: merge_request.project }))
+ = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
%span.light
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index d9400b1d9fa..8163aff43b6 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -19,4 +19,4 @@
.note-search-result
.term
= preserve do
- = search_md_sanitize(markdown(note.note, {no_header_anchors: true}))
+ = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 974751d9970..3b82d8e686f 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -2,15 +2,20 @@
.git-clone-holder.input-group
.input-group-btn
- %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
- %span
- = default_clone_protocol.upcase
- = icon('angle-down')
- %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
- %li
- = ssh_clone_button(project)
- %li
- = http_clone_button(project)
+ -if allowed_protocols_present?
+ .clone-dropdown-btn.btn.btn-static
+ %span
+ = enabled_project_button(project, enabled_protocol)
+ - else
+ %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }}
+ %span
+ = default_clone_protocol.upcase
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
+ %li
+ = ssh_clone_button(project)
+ %li
+ = http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index c38d9313dba..8824bcc158e 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.event-filter
+%ul.nav-links.event-filter.scrolling-tabs
= 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 37dcf39c062..e26693bf5b9 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,13 +1,16 @@
+- repository = nil unless local_assigns.key?(:repository)
+
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- link_icon = icon('link')
+ - link = blob_link if defined?(blob_link)
- 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.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i}
= link_icon
= i
.blob-content{data: {blob_id: blob.id}}
- = highlight(blob.name, blob.data, plain: blob.no_highlighting?)
+ = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 8ff9d4c1c7f..a5df502d7b5 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,4 +1,4 @@
-- if @issues.any?
+- if @issues.reorder(nil).any?
- @issues.group_by(&:project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 9ce5562e667..77676454b57 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,5 +1,15 @@
%span.label-row
+ - if can?(current_user, :admin_label, @project)
+ .draggable-handler
+ = icon('bars')
+ .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
+ dom_id: dom_id(label) } }
+ %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
+ = icon('star-o')
+ %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
+ = icon('star')
%span.label-name
= link_to_label(label, tooltip: false)
- %span.prepend-left-10
- = markdown(label.description, pipeline: :single_line) \ No newline at end of file
+ - if label.description
+ %span.label-description
+ = markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index dc89e36419c..dce492352ac 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,3 +1,9 @@
- labels.each do |label|
- %span.label-row
- = link_to_label(label, tooltip: false)
+ %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
+ = link_to label.name, label_filter_path(@project, label, type: controller.controller_name),
+ class: "btn btn-transparent has-tooltip",
+ style: "background-color: #{label.color};",
+ title: escape_once(label.description),
+ data: { container: "body" }
+ %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
+ = icon("times")
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index e74fc36c797..ca3178395c1 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-- if @merge_requests.any?
+- if @merge_requests.reorder(nil).any?
- @merge_requests.group_by(&:target_project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 1c58345278a..51622931e24 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,8 +1,7 @@
- if @projects.any?
- .prepend-left-10.project-item-select-holder
+ .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]
%b.caret
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index eb2e1919e19..ea7162d4d63 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,7 +1,14 @@
+- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do
- = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm"
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
+ .dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_title "Switch branch/tag"
+ = dropdown_filter "Search branches and tags"
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index d327bd0a96f..249bce926ce 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -6,8 +6,10 @@
- else
= sort_title_recently_created
%b.caret
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
+ = link_to page_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
= link_to page_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to page_filter_path(sort: sort_value_oldest_created) do
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 40c6eb9be45..1ad95351005 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -6,10 +6,10 @@
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
- = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do
- %i.fa.fa-cogs
+ = link_to edit_group_path(group), class: "btn" do
+ = icon('cogs')
- = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
+ = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
= icon('sign-out')
.stats
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
index 1aa7ed1f2eb..427595c47a5 100644
--- a/app/views/shared/groups/_list.html.haml
+++ b/app/views/shared/groups/_list.html.haml
@@ -3,4 +3,4 @@
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
- else
- %h3 No groups found
+ .nothing-here-block No groups found
diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg
new file mode 100644
index 00000000000..d465504b154
--- /dev/null
+++ b/app/views/shared/icons/_activity.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>path-1</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="_activity" fill="#7E7D7D">
+ <g id="Page-1">
+ <g id="path-1">
+ <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_commits.svg b/app/views/shared/icons/_commits.svg
new file mode 100644
index 00000000000..ba9bb89935e
--- /dev/null
+++ b/app/views/shared/icons/_commits.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>Pasted Image 240</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg
new file mode 100644
index 00000000000..adf09a14964
--- /dev/null
+++ b/app/views/shared/icons/_contributionanalytics.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group">
+ <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path>
+ <polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon>
+ <path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path>
+ <path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path>
+ <path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path>
+ <path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_files.svg b/app/views/shared/icons/_files.svg
new file mode 100644
index 00000000000..fc378d81e40
--- /dev/null
+++ b/app/views/shared/icons/_files.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>Pasted Image 237</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Pasted-Image-237">
+ <path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path>
+ <path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path>
+ <polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon>
+ <polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon>
+ <polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon>
+ <polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg
new file mode 100644
index 00000000000..75cae0d16c8
--- /dev/null
+++ b/app/views/shared/icons/_group.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#303030">
+ <path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path>
+ <path d="M5.6667,10.0105 L0.3337,10.0105 C0.1497,10.0105 -0.0003,10.1775 -0.0003,10.3845 L-0.0003,15.6145 C-0.0003,15.8215 0.1497,15.9885 0.3337,15.9885 L5.6667,15.9885 C5.8507,15.9885 5.9997,15.8215 5.9997,15.6145 L5.9997,10.3845 C5.9997,10.1775 5.8507,10.0105 5.6667,10.0105 L5.6667,10.0105 L5.6667,10.0105 Z M1.9997,14.0105 L3.9997,14.0105 L3.9997,12.0105 L1.9997,12.0105 L1.9997,14.0105 L1.9997,14.0105 Z" id="Fill-8"></path>
+ <polygon id="Stroke-1" points="12.5 7.5834 3.5 7.5834 3.5 9.5834 12.5 9.5834"></polygon>
+ <polygon id="Stroke-3" points="9 9.0834 9 5.0834 7 5.0834 7 9.0834"></polygon>
+ <polygon id="Stroke-4" points="4 11.0834 4 7.5834 2 7.5834 2 11.0834"></polygon>
+ <polygon id="Stroke-6" points="14 11.0834 14 7.5834 12 7.5834 12 11.0834"></polygon>
+ <path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_issues.svg.erb b/app/views/shared/icons/_issues.svg.erb
new file mode 100644
index 00000000000..fa8655b5609
--- /dev/null
+++ b/app/views/shared/icons/_issues.svg.erb
@@ -0,0 +1,4 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16" class="gitlab-icon">
+ <path fill="#7E7C7C" d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2"></path>
+ <path fill="#7E7C7C" d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z"></path>
+</svg>
diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg
new file mode 100644
index 00000000000..f8043b31fe8
--- /dev/null
+++ b/app/views/shared/icons/_members.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#7E7C7C">
+ <path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path>
+ <path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg
new file mode 100644
index 00000000000..3d62ecc0631
--- /dev/null
+++ b/app/views/shared/icons/_milestones.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#7E7C7C">
+ <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
+ <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
+ <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
+ <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg
new file mode 100644
index 00000000000..dd3dbcc4473
--- /dev/null
+++ b/app/views/shared/icons/_mr.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#7E7C7C">
+ <path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path>
+ <path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_pipelines.svg b/app/views/shared/icons/_pipelines.svg
new file mode 100644
index 00000000000..794e8a27025
--- /dev/null
+++ b/app/views/shared/icons/_pipelines.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>Pasted Image 246</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg
new file mode 100644
index 00000000000..1e8b43f8c6b
--- /dev/null
+++ b/app/views/shared/icons/_project.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>Page 1</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E"></path>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg
new file mode 100644
index 00000000000..182d91e23aa
--- /dev/null
+++ b/app/views/shared/icons/_wiki.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+ <title>Pasted Image 241</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 9474462cbd1..094d6636c66 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,6 +1,8 @@
.issues-filters
.issues-details-filters.row-content-block.second-block
- = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:issue_search].present?
+ = hidden_field_tag :issue_search, params[:issue_search]
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
@@ -29,7 +31,7 @@
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
- = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
+ = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
@@ -42,6 +44,10 @@
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 5c52cc6d1da..c30bdb0ae91 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -35,54 +35,62 @@
.clearfix
.error-alert
-- if issuable.is_a?(Issue) && !issuable.project.private?
+- if issuable.is_a?(Issue)
.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
+ This issue is confidential and should only be visible to team members with at least Reporter access.
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ - has_due_date = issuable.has_attribute?(:due_date)
%hr
- .form-group
- .issue-assignee
- = f.label :assignee_id, "Assignee", class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
- selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true, include_blank: true)
- &nbsp;
- = link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
- .form-group
- .issue-milestone
- = f.label :milestone_id, "Milestone", class: 'control-label'
- .col-sm-10
- - if milestone_options(issuable).present?
+ .row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = f.select(:milestone_id, milestone_options(issuable),
- { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- - else
- .prepend-top-10
- %span.light No open milestones available.
- &nbsp;
- - if can? current_user, :admin_milestone, issuable.project
- = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
- .form-group
- - has_labels = issuable.project.labels.any?
- = f.label :label_ids, "Labels", class: 'control-label'
- .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- - if has_labels
- .issuable-form-select-holder
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- - else
- %span.light No labels yet.
- &nbsp;
- - if can? current_user, :admin_label, issuable.project
- = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
+ = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
+ placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
+ selected: issuable.assignee_id, project: @target_project || @project,
+ first_user: true, current_user: true, include_blank: true)
+ %div
+ = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline'
+ .form-group.issue-milestone
+ = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ - if milestone_options(issuable).present?
+ .issuable-form-select-holder
+ = f.select(:milestone_id, milestone_options(issuable),
+ { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
+ - else
+ .prepend-top-10
+ %span.light No open milestones available.
+ - if can? current_user, :admin_milestone, issuable.project
+ %div
+ = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .form-group
+ - has_labels = issuable.project.labels.any?
+ = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ - if has_labels
+ .issuable-form-select-holder
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
+ - else
+ %span.light No labels yet.
+ - if can? current_user, :admin_label, issuable.project
+ %div
+ = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = f.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
- if issuable.can_move?(current_user)
%hr
@@ -90,9 +98,7 @@
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- - projects = project_options(issuable, current_user, ability: :admin_issue)
- = select_tag(:move_to_project_id, projects, include_blank: true,
- class: 'select2', data: { placeholder: 'Select project' })
+ = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
@@ -114,6 +120,13 @@
- if @merge_request.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
+ - if @merge_request.can_remove_source_branch?(current_user)
+ .form-group
+ .col-sm-10.col-sm-offset-2
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 61fd1e9c335..d34d28f6736 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,14 +1,25 @@
+- show_create = local_assigns.fetch(:show_create, true)
+- extra_options = local_assigns.fetch(:extra_options, true)
+- filter_submit = local_assigns.fetch(:filter_submit, true)
+- show_footer = local_assigns.fetch(:show_footer, true)
+- data_options = local_assigns.fetch(:data_options, {})
+- classes = local_assigns.fetch(:classes, [])
+- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
+- dropdown_data.merge!(data_options)
+- classes << 'js-extra-options' if extra_options
+- classes << 'js-filter-submit' if filter_submit
+
- if params[:label_name].present?
- if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text
= h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" }
- - if can? current_user, :admin_label, @project and @project
+ = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
+ - if show_create and @project and can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 7f4867417f7..0acb8253139 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,20 +1,22 @@
- title = local_assigns.fetch(:title, 'Assign labels')
+- show_create = local_assigns.fetch(:show_create, true)
+- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
.dropdown-page-one
= dropdown_title(title)
- = dropdown_filter(filter_placeholder)
+ = dropdown_filter(filter_placeholder, search_id: "label-name")
= dropdown_content
- - if @project
+ - if @project && show_footer
= dropdown_footer do
%ul.dropdown-footer-list
- - if can? current_user, :admin_label, @project
+ - 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), :"data-is-link" => true do
- - if can? current_user, :admin_label, @project
+ - if show_create && @project && can?(current_user, :admin_label, @project)
Manage labels
- else
View labels
- = dropdown_loading \ No newline at end of file
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index afad48499b7..186963b32b8 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,8 +1,2 @@
= 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 ed1b8a8da2a..adfab1af53e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,24 +1,21 @@
+- todo = issuable_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
- %span.issuable-count.hide-collapsed.pull-left
- = issuable.iid
- of
- = issuables_count(issuable)
- %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ - if current_user
+ %span.issuable-header-text.hide-collapsed.pull-left
+ Todo
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= 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 issuable-pager'
- - else
- %a.btn.btn-default.issuable-pager.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 issuable-pager'
- - else
- %a.btn.btn-default.issuable-pager.disabled{href: '#'}
- Next
+ - if current_user
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+ %span.js-issuable-todo-text
+ - if todo
+ Mark Done
+ - else
+ Add Todo
+ = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
@@ -32,20 +29,21 @@
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
+ .value.hide-collapsed
- if issuable.assignee
- = link_to_member(@project, issuable.assignee, size: 32) do
+ = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle')
%span.username
= issuable.assignee.to_reference
- else
- %span.assign-yourself
+ %span.assign-yourself.no-value
No assignee
- if can_edit_issuable
+ \-
%a.js-assign-yourself{ href: '#' }
- \- assign yourself
+ assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
@@ -56,7 +54,8 @@
= icon('clock-o')
%span
- if issuable.milestone
- = issuable.milestone.title
+ %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}}
+ = issuable.milestone.title
- else
None
.title.hide-collapsed
@@ -64,12 +63,11 @@
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
+ .value.hide-collapsed
- if issuable.milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- = issuable.milestone.title
+ = link_to issuable.milestone.title, namespace_project_milestone_path(@project.namespace, @project, issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
- else
- .light None
+ %span.no-value None
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
@@ -86,11 +84,17 @@
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
- - if issuable.due_date
- = issuable.due_date.to_s(:medium)
- - else
- .light None
+ .value.hide-collapsed
+ %span.value-content
+ - if issuable.due_date
+ %span.bold= issuable.due_date.to_s(:medium)
+ - else
+ %span.no-value No due date
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) }
+ \-
+ %a.js-remove-due-date{ href: "#", role: "button" }
+ remove due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable.due_date
@@ -108,20 +112,20 @@
.sidebar-collapsed-icon
= icon('tags')
%span
- = issuable.labels.count
+ = issuable.labels_array.size
.title.hide-collapsed
Labels
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
- - if issuable.labels.any?
- - issuable.labels.each do |label|
+ .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
+ - if issuable.labels_array.any?
+ - issuable.labels_array.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
- .light None
+ %span.no-value None
.selectbox.hide-collapsed
- - issuable.labels.each do |label|
+ - issuable.labels_array.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
@@ -142,7 +146,7 @@
.title.hide-collapsed
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
new file mode 100644
index 00000000000..eff914398bb
--- /dev/null
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -0,0 +1,10 @@
+- if can?(current_user, :request_access, source)
+ - if requester = source.requesters.find_by(user_id: current_user.id)
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'btn'
+ - else
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'btn'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
new file mode 100644
index 00000000000..5ae485f36ba
--- /dev/null
+++ b/app/views/shared/members/_member.html.haml
@@ -0,0 +1,80 @@
+- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
+- show_controls = local_assigns.fetch(:show_controls, true)
+- user = member.user
+
+%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
+ - if show_roles
+ .controls
+ %strong.control-text= member.human_access
+ - if show_controls
+ - if !user && can?(current_user, action_member_permission(:admin, member), member.source)
+ = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
+ method: :post,
+ class: 'btn'
+
+ - if can?(current_user, action_member_permission(:update, member), member)
+ = button_tag icon('pencil'),
+ type: 'button',
+ class: 'btn inline js-toggle-button',
+ title: 'Edit access level'
+
+ - if member.request?
+ = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
+ method: :post,
+ class: 'btn btn-success',
+ title: 'Grant access'
+
+ - if can?(current_user, action_member_permission(:destroy, member), member)
+ - if current_user == user
+ = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(member.source) },
+ class: 'btn btn-remove'
+ - else
+ = link_to icon('trash'), member,
+ remote: true,
+ method: :delete,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn btn-remove',
+ title: remove_member_title(member)
+
+
+ %span{ class: ("list-item-name" if show_controls) }
+ - if user
+ = image_tag avatar_icon(user, 40), class: "avatar s40", alt: ''
+ %strong
+ = link_to user.name, user_path(user)
+ %span.cgray= user.username
+
+ - if user == current_user
+ %span.label.label-success It's you
+
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+
+ .cgray
+ - if member.request?
+ Requested
+ = time_ago_with_tooltip(member.requested_at)
+ - else
+ Joined #{time_ago_with_tooltip(member.created_at)}
+
+ - else
+ = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
+ %strong= member.invite_email
+ .cgray
+ Invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if show_roles
+ .edit-member.hide.js-toggle-content
+ %br
+ = form_for member, remote: true do |f|
+ .prepend-top-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
new file mode 100644
index 00000000000..40b39e850b0
--- /dev/null
+++ b/app/views/shared/members/_requests.html.haml
@@ -0,0 +1,8 @@
+- if requesters.any?
+ .panel.panel-default
+ .panel-heading
+ %strong= membership_source.name
+ access requests
+ %span.badge= requesters.size
+ %ul.content-list
+ = render partial: 'shared/members/member', collection: requesters, as: :member
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 47b66d44e43..3c03c220ddd 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,7 +21,8 @@
= 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', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
+ %span{ class: "assignee-icon" }
+ - if assignee
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
+ class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
+ - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
index c29d8ee6737..9c193f901e2 100644
--- a/app/views/shared/milestones/_merge_requests_tab.haml
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -3,10 +3,10 @@
.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')
+ = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
+ = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
+ = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
+ = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 6b25745c554..acc3ccf4dcf 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -35,11 +35,9 @@
.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')
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
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')
+ = 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 btn-grouped"
+ = 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 btn-grouped" do
Delete
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
index 67ae85ac276..549d2e2f61e 100644
--- a/app/views/shared/milestones/_participants_tab.html.haml
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -3,6 +3,6 @@
%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)
+ %strong= truncate(user.name, length: 40)
%br
%small.cgray= user.username
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 385c6596606..975c74f4ea6 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -10,6 +10,13 @@
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
closed
+ %strong= milestone.merge_requests.size
+ merge requests:
+ %span.milestone-stat
+ %strong= milestone.merge_requests.opened.size
+ open and
+ %strong= milestone.merge_requests.merged.size
+ merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
new file mode 100644
index 00000000000..ff1cf966a9b
--- /dev/null
+++ b/app/views/shared/notifications/_button.html.haml
@@ -0,0 +1,25 @@
+- left_align = local_assigns[:left_align]
+- if notification_setting
+ .dropdown.notification-dropdown.pull-right
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ = f.hidden_field :level, class: "notification_setting_level"
+ .js-notification-toggle-btns
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
+ %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
+ = icon("bell", class: "js-notification-loading")
+ = notification_title(notification_setting.level)
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %span.caret
+ .sr-only Toggle dropdown
+ - else
+ %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ = icon("bell", class: "js-notification-loading")
+ = notification_title(notification_setting.level)
+ = icon("caret-down")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+
+ = content_for :scripts_body do
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
new file mode 100644
index 00000000000..b704981e3db
--- /dev/null
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -0,0 +1,31 @@
+.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } } ×
+ %h4#custom-notifications-title.modal-title
+ Custom notification events
+
+ .modal-body
+ .container-fluid
+ = form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ .row
+ .col-lg-4
+ %h4.prepend-top-0
+ Notification events
+ %p
+ Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
+ = succeed "." do
+ %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ .col-lg-8
+ - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
+ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
+ .form-group
+ .checkbox{ class: ("prepend-top-0" if index == 0) }
+ %label{ for: field_id }
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
+ %strong
+ = event.to_s.humanize
+ = icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
new file mode 100644
index 00000000000..d3258ee64cb
--- /dev/null
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -0,0 +1,13 @@
+- left_align = local_assigns[:left_align]
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+ - NotificationSetting.levels.each_key do |level|
+ - next if level == "custom"
+ - next if level == "global" && notification_setting.source.nil?
+
+ = notification_list_item(level, notification_setting)
+
+ %li.divider
+ %li
+ %a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
+ %strong.dropdown-menu-inner-title Custom
+ %span.dropdown-menu-inner-content= notification_description("custom")
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 1169bed0382..b7f8551153b 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,31 +1,30 @@
- @sort ||= sort_value_recently_updated
- personal = params[:personal]
- archived = params[:archived]
+- namespace_id = params[:namespace_id]
.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- = projects_sort_options_hash[@sort]
- %b.caret
+ - toggle_text = projects_sort_options_hash[@sort]
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
%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, personal: personal), class: ("is-active" if @sort == value) do
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), 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
+ = link_to filter_projects_path(namespace_id: namespace_id, 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
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
Show archived projects
- if current_user
%li.divider
%li
- = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do
Owned by anyone
%li
- = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
Owned by me
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 2e08bb2ac08..3a9dd37dc7d 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -16,6 +16,12 @@
= 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
+
+ - if @private_forks_count && @private_forks_count > 0
+ %li.project-row.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.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.nothing-here-block No projects found
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index ab8b022411d..b8b66d08db8 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,12 +12,9 @@
%li.project-row{ class: css_class }
= cache(cache_key) do
.controls
- - if project.main_language
- %span
- = project.main_language
- if project.commit.try(:status)
%span
- = render_ci_status(project.commit)
+ = render_commit_status(project.commit)
- if forks
%span
= icon('code-fork')
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index e65b1814872..af753496260 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -1,25 +1,24 @@
-.detail-page-header
- .snippet-box.has-tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }}
+.detail-page-header.clearfix
+ .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
+ %span.sr-only
+ = visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level, fw: false)
- = visibility_level_label(@snippet.visibility_level)
- %span.identifier
- Snippet ##{@snippet.id}
+ %strong.item-title
+ Snippet #{@snippet.to_reference}
%span.creator
- &middot; created by #{link_to_member(@project, @snippet.author, size: 24)}
- &middot;
+ created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")}
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- if @snippet.updated_at != @snippet.created_at
%span
- &middot;
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
- .pull-right
+ .snippet-actions
- if @snippet.project_id?
= render "projects/snippets/actions"
- else
= render "snippets/actions"
-.detail-page-description.row-content-block.second-block
- %h2.title
- = markdown escape_once(@snippet.title), pipeline: :single_line
+.content-block.second-block
+ %h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
new file mode 100644
index 00000000000..d1e861ca80c
--- /dev/null
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -0,0 +1,91 @@
+- page_title "Webhooks"
+- context_title = @project ? 'project' : 'group'
+
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be
+ used for binding events when something is happening within the project.
+ .col-lg-9.append-bottom-default
+ = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
+ = form_errors(hook)
+
+ .form-group
+ = f.label :url, "URL", class: 'label-light'
+ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
+ .form-group
+ = f.label :token, "Secret Token", class: 'label-light'
+ = f.text_field :token, class: "form-control", placeholder: ''
+ %p.help-block
+ Use this token to validate received payloads
+ .form-group
+ = f.label :url, "Trigger", class: 'label-light'
+ %ul.list-unstyled
+ %li
+ = f.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ %li
+ = f.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+ %li
+ = f.check_box :note_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :note_events, class: 'list-label' do
+ %strong Comments
+ %p.light
+ This url will be triggered when someone adds a comment
+ %li
+ = f.check_box :issues_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :issues_events, class: 'list-label' do
+ %strong Issues events
+ %p.light
+ This url will be triggered when an issue is created/updated/merged
+ %li
+ = 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/updated/merged
+ %li
+ = f.check_box :build_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :build_events, class: 'list-label' do
+ %strong Build events
+ %p.light
+ This url will be triggered when the build status changes
+ %li
+ = f.check_box :wiki_page_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :wiki_page_events, class: 'list-label' do
+ %strong Wiki Page events
+ %p.light
+ This url will be triggered when a wiki page is created/updated
+ .form-group
+ = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
+ .checkbox
+ = f.label :enable_ssl_verification do
+ = f.check_box :enable_ssl_verification
+ %strong Enable SSL verification
+ = f.submit "Add Webhook", class: "btn btn-create"
+ %hr
+ %h5.prepend-top-default
+ Webhooks (#{hooks.count})
+ - if hooks.any?
+ %ul.well-list
+ - hooks.each do |hook|
+ = render "project_hook", hook: hook
+ - else
+ %p.settings-message.text-center.append-bottom-0
+ No webhooks found, add one in the form above.
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
index 5c9294c0ab5..30e956e5f40 100644
--- a/app/views/sherlock/queries/_backtrace.html.haml
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -6,7 +6,11 @@
%ul.well-list
- @query.application_backtrace.each do |location|
%li
- = location.path
+ %strong
+ - if defined?(BetterErrors)
+ = link_to(location.path, BetterErrors.editor[location.path, location.line])
+ - else
+ = location.path
%small.light
= t('sherlock.line')
= location.line
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 549b47430e6..7073c0f4d90 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -11,13 +11,17 @@
= @query.duration.round(4)
= t('sherlock.milliseconds')
%li
+ - frame = @query.last_application_frame
%span.light
#{t('sherlock.origin')}:
%strong
- = @query.last_application_frame.path
+ - if defined?(BetterErrors)
+ = link_to(frame.path, BetterErrors.editor[frame.path, frame.line])
+ - else
+ = frame.path
%small.light
= t('sherlock.line')
- = @query.last_application_frame.line
+ = frame.line
.panel.panel-default
.panel-heading
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 1979ae6d5bc..a7769654b61 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,11 +1,27 @@
-= link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do
- = icon('plus')
- New Snippet
-- if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
- = icon('pencil-square-o')
- Edit
-- if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do
- = icon('trash-o')
- Delete
+.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do
+ = icon('plus')
+ New Snippet
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ Edit
+ - if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
+ Delete
+.visible-xs-block.dropdown
+ %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
+ Options
+ %span.caret
+ .dropdown-menu.dropdown-menu-full-width
+ %ul
+ %li
+ = link_to new_snippet_path, title: "New Snippet" do
+ New Snippet
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
+ - if can?(current_user, :admin_personal_snippet, @snippet)
+ %li
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
+ Delete
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index a2b36568770..ed3992650d4 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,11 +3,10 @@
.snippet-holder
= render 'shared/snippets/header'
- %article.file-holder
- .file-title
+ %article.file-holder.file-holder-no-border.snippet-file-content
+ .file-title.file-title-clear
= blob_icon 0, @snippet.file_name
- %strong
- = @snippet.file_name
+ = @snippet.file_name
.file-actions.hidden-xs
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
new file mode 100644
index 00000000000..75fb0e303ad
--- /dev/null
+++ b/app/views/u2f/_authenticate.html.haml
@@ -0,0 +1,28 @@
+#js-authenticate-u2f
+
+%script#js-authenticate-u2f-not-supported{ type: "text/template" }
+ %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
+
+%script#js-authenticate-u2f-setup{ type: "text/template" }
+ %div
+ %p Insert your security key (if you haven't already), and press the button below.
+ %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
+
+%script#js-authenticate-u2f-in-progress{ type: "text/template" }
+ %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
+
+%script#js-authenticate-u2f-error{ type: "text/template" }
+ %div
+ %p <%= error_message %>
+ %a.btn.btn-warning#js-u2f-try-again Try again?
+
+%script#js-authenticate-u2f-authenticated{ type: "text/template" }
+ %div
+ %p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
+ = form_tag(new_user_session_path, method: :post) do |f|
+ = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
+ = submit_tag "Authenticate via U2F Device", class: "btn btn-success"
+
+:javascript
+ var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
+ u2fAuthenticate.start();
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
new file mode 100644
index 00000000000..cbb8dfb7829
--- /dev/null
+++ b/app/views/u2f/_register.html.haml
@@ -0,0 +1,38 @@
+#js-register-u2f
+
+%script#js-register-u2f-not-supported{ type: "text/template" }
+ %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
+
+%script#js-register-u2f-setup{ type: "text/template" }
+ - if current_user.two_factor_otp_enabled?
+ .row.append-bottom-10
+ .col-md-3
+ %button#js-setup-u2f-device.btn.btn-info Setup New U2F Device
+ .col-md-9
+ %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
+ - else
+ .row.append-bottom-10
+ .col-md-3
+ %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup New U2F Device
+ .col-md-9
+ %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
+
+%script#js-register-u2f-in-progress{ type: "text/template" }
+ %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
+
+%script#js-register-u2f-error{ type: "text/template" }
+ %div
+ %p
+ %span <%= error_message %>
+ %a.btn.btn-warning#js-u2f-try-again Try again?
+
+%script#js-register-u2f-registered{ type: "text/template" }
+ %div.row.append-bottom-10
+ %p Your device was successfully set up! Click this button to register with the GitLab server.
+ = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
+ = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
+ = submit_tag "Register U2F Device", class: "btn btn-success"
+
+:javascript
+ var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
+ u2fRegister.start();
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 1de71f37d1a..77f2ddefb1e 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,10 +1,9 @@
-#cal-heatmap.calendar
- :javascript
- new Calendar(
- #{@timestamps.to_json},
- #{@starting_year},
- #{@starting_month},
- '#{user_calendar_activities_path}'
- );
-
-.calendar-hint Summary of issues, merge requests, and push events
+.clearfix.calendar
+ .js-contrib-calendar
+ .calendar-hint
+ Summary of issues, merge requests, and push events
+:javascript
+ new Calendar(
+ #{@timestamps.to_json},
+ '#{user_calendar_activities_path}'
+ );
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 027a93a75fc..f51599212db 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -1,23 +1,27 @@
%h4.prepend-top-20
- %span.light Contributions for
+ Contributions for
%strong #{@calendar_date.to_s(:short)}
-%ul.bordered-list
- - @events.sort_by(&:created_at).each do |event|
- %li
- %span.light
- %i.fa.fa-clock-o
- = event.created_at.to_s(:time)
- - if event.push?
- #{event.action_name} #{event.ref_type} #{event.ref_name}
- - else
- = event_action_name(event)
- - if event.target
- %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
-
- at
- %strong
- - if event.project
- = link_to_project event.project
+- if @events.any?
+ %ul.bordered-list
+ - @events.sort_by(&:created_at).each do |event|
+ %li
+ %span.light
+ %i.fa.fa-clock-o
+ = event.created_at.to_s(:time)
+ - if event.push?
+ #{event.action_name} #{event.ref_type} #{event.ref_name}
- else
- = event.project_name
+ = event_action_name(event)
+ - if event.target
+ %strong= link_to "#{event.target.to_reference}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+
+ at
+ %strong
+ - if event.project
+ = link_to_project event.project
+ - else
+ = event.project_name
+- else
+ %p
+ No contributions found for #{@calendar_date.to_s(:short)}
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index e9e466c6350..6c85e5f9fbd 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.id user_url(@user)
xml.updated @events[0].updated_at.xmlschema if @events[0]
- @events.each do |event|
- event_to_atom(xml, event)
- end
+ xml << render(@events) if @events.any?
end
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 3c0b89c6741..db2b4885861 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,5 +1,8 @@
- page_title @user.name
- page_description @user.bio
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/d3.js')
+ = page_specific_javascript_tag('users/application.js')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -26,6 +29,11 @@
&nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
+ - if current_user.admin?
+ &nbsp;
+ = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('users')
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
@@ -78,18 +86,20 @@
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
Contributed projects
- %li.projects-tab
+ %li.js-projects-tab
= link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
Personal projects
+ %li.js-snippets-tab
+ = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
+ Snippets
%div{ class: container_class }
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
- %div{ class: container_class }
- .user-calendar{data: {href: user_calendar_path}}
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
+ .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} }
@@ -104,6 +114,9 @@
#projects.tab-pane
- # This tab is always loaded via AJAX
+ #snippets.tab-pane
+ - # This tab is always loaded via AJAX
+
.loading-status
= spinner
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
deleted file mode 100644
index 4beb8746444..00000000000
--- a/app/views/votes/_votes_block.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-.awards.votes-block
- - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
- %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
- = emoji_icon(emoji, sprite: false)
- %span.award-control-text.js-counter
- = notes.count
-
- - if current_user
- %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
- :javascript
- var getEmojisUrl = "#{emojis_path}";
- var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
- var noteableType = "#{votable.class.name.underscore}";
- var noteableId = "#{votable.id}";
- var unicodes = #{AwardEmoji.unicode.to_json};
-
- window.awardsHandler = new AwardsHandler(
- getEmojisUrl,
- postEmojiUrl,
- noteableType,
- noteableId,
- unicodes
- );
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index c4d8595d45d..8551288e2f2 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,6 +1,9 @@
class EmailsOnPushWorker
include Sidekiq::Worker
+ sidekiq_options queue: :mailers
+ attr_reader :email, :skip_premailer
+
def perform(project_id, recipients, push_data, options = {})
options.symbolize_keys!
options.reverse_merge!(
@@ -25,42 +28,73 @@ class EmailsOnPushWorker
:push
end
+ merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha)
+
+ diff_refs = nil
compare = nil
reverse_compare = false
if action == :push
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
+ diff_refs = Gitlab::Diff::DiffRefs.new(
+ base_sha: merge_base_sha,
+ start_sha: before_sha,
+ head_sha: after_sha
+ )
+
return false if compare.same
if compare.commits.empty?
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
+ diff_refs = Gitlab::Diff::DiffRefs.new(
+ base_sha: merge_base_sha,
+ start_sha: after_sha,
+ head_sha: before_sha
+ )
+
reverse_compare = true
return false if compare.commits.empty?
end
end
- recipients.split(" ").each do |recipient|
+ recipients.split.each do |recipient|
begin
- Notify.repository_push_email(
- project_id,
+ send_email(
recipient,
- author_id: author_id,
- ref: ref,
- action: action,
- compare: compare,
- reverse_compare: reverse_compare,
- send_from_committer_email: send_from_committer_email,
- disable_diffs: disable_diffs
- ).deliver_now
+ project_id,
+ author_id: author_id,
+ ref: ref,
+ action: action,
+ compare: compare,
+ reverse_compare: reverse_compare,
+ diff_refs: diff_refs,
+ send_from_committer_email: send_from_committer_email,
+ disable_diffs: disable_diffs
+ )
+
# These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}")
end
end
ensure
+ @email = nil
compare = nil
GC.start
end
+
+ private
+
+ def send_email(recipient, project_id, options)
+ # Generating the body of this email can be expensive, so only do it once
+ @skip_premailer ||= email.present?
+ @email ||= Notify.repository_push_email(project_id, options)
+
+ email.to = recipient
+ email.add_message_id
+ email.header[:skip_premailer] = true if skip_premailer
+ email.deliver_now
+ end
end
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
new file mode 100644
index 00000000000..c64ea108d52
--- /dev/null
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -0,0 +1,13 @@
+class ExpireBuildArtifactsWorker
+ include Sidekiq::Worker
+
+ def perform
+ Rails.logger.info 'Cleaning old build artifacts'
+
+ builds = Ci::Build.with_expired_artifacts
+ builds.find_each(batch_size: 50).each do |build|
+ Rails.logger.debug "Removing artifacts build #{build.id}..."
+ build.erase_artifacts!
+ end
+ end
+end
diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb
new file mode 100644
index 00000000000..1d91897d520
--- /dev/null
+++ b/app/workers/gitlab_remove_project_export_worker.rb
@@ -0,0 +1,9 @@
+class GitlabRemoveProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ Project.remove_gitlab_exports!
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index f3327ca9e61..09035a7cf2d 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -4,10 +4,10 @@ class PostReceive
sidekiq_options queue: :post_receive
def perform(repo_path, identifier, changes)
- if repo_path.start_with?(Gitlab.config.gitlab_shell.repos_path.to_s)
- repo_path.gsub!(Gitlab.config.gitlab_shell.repos_path.to_s, "")
+ if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
+ repo_path.gsub!(path[1].to_s, "")
else
- log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"")
+ log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
end
post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
new file mode 100644
index 00000000000..39f6037e077
--- /dev/null
+++ b/app/workers/project_export_worker.rb
@@ -0,0 +1,12 @@
+class ProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :gitlab_shell, retry: true
+
+ def perform(current_user_id, project_id)
+ current_user = User.find(current_user_id)
+ project = Project.find(project_id)
+
+ ::Projects::ImportExport::ExportService.new(project, current_user).execute
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index f2d12ba5a7d..98ddf5d0688 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -15,7 +15,7 @@ module RepositoryCheck
private
def check(project)
- if !git_fsck(project.repository)
+ if has_pushes?(project) && !git_fsck(project.repository)
false
elsif project.wiki_enabled?
# Historically some projects never had their wiki repos initialized;
@@ -44,5 +44,9 @@ module RepositoryCheck
false
end
end
+
+ def has_pushes?(project)
+ Project.with_push.exists?(project.id)
+ end
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index f9e32337983..f7604e48f83 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -12,11 +12,10 @@ class RepositoryForkWorker
return
end
- result = gitlab_shell.fork_repository(source_path, target_path)
+ result = gitlab_shell.fork_repository(project.repository_storage_path, source_path, target_path)
unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
- project.update(import_error: "The project could not be forked.")
- project.import_fail
+ project.mark_import_as_failed('The project could not be forked.')
return
end
@@ -24,8 +23,7 @@ class RepositoryForkWorker
unless project.valid_repo?
logger.error("Project #{project_id} had an invalid repository after fork")
- project.update(import_error: "The forked repository is invalid.")
- project.import_fail
+ project.mark_import_as_failed('The forked repository is invalid.')
return
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 2937493c614..7d819fe78f8 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -13,8 +13,7 @@ class RepositoryImportWorker
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
- project.update(import_error: result[:message])
- project.import_fail
+ project.mark_import_as_failed(result[:message])
return
end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
index ca594e77e7c..6828013b377 100644
--- a/app/workers/stuck_ci_builds_worker.rb
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -6,7 +6,7 @@ class StuckCiBuildsWorker
def perform
Rails.logger.info 'Cleaning stuck builds'
- builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
+ builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
builds.find_each(batch_size: 50).each do |build|
Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
build.drop
diff --git a/bin/spring b/bin/spring
index 7fe232c3aae..e0d140fe0c7 100755
--- a/bin/spring
+++ b/bin/spring
@@ -3,7 +3,7 @@
# This file loads spring without using Bundler, in order to be fast.
# It gets overwritten when you run the `spring binstub` command.
-unless defined?(Spring)
+unless (defined?(Spring) || ENV['ENABLE_SPRING'] != '1') && File.basename($0) != 'spring'
require 'rubygems'
require 'bundler'
diff --git a/config/application.rb b/config/application.rb
index cba80f38f1f..2b0595ede2b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,6 +26,8 @@ module Gitlab
#{config.root}/app/models/members
#{config.root}/app/models/project_services))
+ config.generators.templates.push("#{config.root}/generator_templates")
+
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
@@ -39,7 +41,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
- #
+ #
# Parameters filtered:
# - Password (:password, :password_confirmation)
# - Private tokens (:private_token)
@@ -78,6 +80,12 @@ module Gitlab
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
+ config.assets.precompile << "mailers/*.css"
+ config.assets.precompile << "graphs/application.js"
+ config.assets.precompile << "users/application.js"
+ config.assets.precompile << "network/application.js"
+ config.assets.precompile << "lib/utils/*.js"
+ config.assets.precompile << "lib/*.js"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/boot.rb b/config/boot.rb
index 4489e58688c..f2830ae3166 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -3,4 +3,4 @@ require 'rubygems'
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
new file mode 100644
index 00000000000..293f2b71d65
--- /dev/null
+++ b/config/dependency_decisions.yml
@@ -0,0 +1,189 @@
+---
+# IGNORED GROUPS AND GEMS
+- - :ignore_group
+ - development
+ - :who: Connor Shea
+ :why: Development gems are not distributed with the final product and are therefore exempt.
+ :versions: []
+ :when: 2016-04-17 21:27:01.054140000 Z
+- - :ignore_group
+ - test
+ - :who: Connor Shea
+ :why: Test gems are not distributed with the final product and are therefore exempt.
+ :versions: []
+ :when: 2016-04-17 21:27:06.250326000 Z
+- - :ignore
+ - bundler
+ - :who: Connor Shea
+ :why: Bundler is MIT licensed but will sometimes fail in CI.
+ :versions: []
+ :when: 2016-05-02 06:42:08.045090000 Z
+
+# LICENSE WHITELIST
+- - :whitelist
+ - MIT
+ - :who: Connor Shea
+ :why: http://choosealicense.com/licenses/mit/
+ :versions: []
+ :when: 2016-04-17 21:12:24.558441000 Z
+- - :whitelist
+ - Apache 2.0
+ - :who: Connor Shea
+ :why: http://choosealicense.com/licenses/apache-2.0/
+ :versions: []
+ :when: 2016-05-02 05:27:43.762702000 Z
+- - :whitelist
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/ruby/ruby/blob/ruby_2_1/COPYING
+ :versions: []
+ :when: 2016-05-02 05:31:54.498490000 Z
+- - :whitelist
+ - LGPL
+ - :who: Connor Shea
+ :why: http://www.gnu.org/licenses/license-list.html#LGPLv2.1
+ :versions: []
+ :when: 2016-05-02 05:32:48.645841000 Z
+- - :whitelist
+ - ISC
+ - :who: Connor Shea
+ :why: http://www.gnu.org/licenses/license-list.html#ISC
+ :versions: []
+ :when: 2016-05-02 05:42:01.894452000 Z
+- - :whitelist
+ - New BSD
+ - :who: Connor Shea
+ :why: https://opensource.org/licenses/BSD-3-Clause
+ :versions: []
+ :when: 2016-05-02 05:44:38.246021000 Z
+- - :whitelist
+ - LGPL-2.1+
+ - :who: Connor Shea
+ :why: Equivalent to LGPL.
+ :versions: []
+ :when: 2016-05-02 05:52:56.303239000 Z
+- - :whitelist
+ - BSD
+ - :who: Connor Shea
+ :why: https://opensource.org/licenses/BSD-2-Clause
+ :versions: []
+ :when: 2016-05-02 05:55:09.796363000 Z
+
+# LICENSE BLACKLIST
+- - :blacklist
+ - GPLv2
+ - :who: Connor Shea
+ :why: GPL-licensed libraries cannot be linked to from non-GPL projects.
+ :versions: []
+ :when: 2016-05-02 05:29:27.637336000 Z
+- - :blacklist
+ - GPLv3
+ - :who: Connor Shea
+ :why: GPL-licensed libraries cannot be linked to from non-GPL projects.
+ :versions: []
+ :when: 2016-05-02 05:29:43.904715000 Z
+
+# GEM LICENSES
+- - :license
+ - raphael-rails
+ - MIT
+ - :who: Connor Shea
+ :why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt
+ :versions: []
+ :when: 2016-04-17 21:30:07.575392000 Z
+- - :license
+ - rouge
+ - MIT
+ - :who: Connor Shea
+ :why: https://github.com/jneen/rouge/blob/master/LICENSE
+ :versions: []
+ :when: 2016-04-17 21:31:29.490394000 Z
+- - :license
+ - pyu-ruby-sasl
+ - MIT
+ - :who: Connor Shea
+ :why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE
+ :versions: []
+ :when: 2016-04-17 21:41:55.266420000 Z
+- - :license
+ - six
+ - MIT
+ - :who: Connor Shea
+ :why: https://github.com/randx/six/blob/master/LICENSE
+ :versions: []
+ :when: 2016-04-17 21:42:31.420186000 Z
+- - :license
+ - rdoc
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc
+ :versions: []
+ :when: 2016-04-17 21:43:30.480413000 Z
+- - :license
+ - expression_parser
+ - MIT
+ - :who: Connor Shea
+ :why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE
+ :versions: []
+ :when: 2016-04-17 21:45:41.829912000 Z
+- - :license
+ - creole
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/minad/creole#license
+ :versions: []
+ :when: 2016-04-17 21:49:10.329759000 Z
+- - :license
+ - eventmachine
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE
+ :versions: []
+ :when: 2016-04-17 21:49:10.329759001 Z
+- - :license
+ - unicorn
+ - ruby
+ - :who: Connor Shea
+ :why: http://unicorn.bogomips.org/LICENSE.html
+ :versions: []
+ :when: 2016-05-02 05:45:28.817510000 Z
+- - :license
+ - unicorn-worker-killer
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/kzk/unicorn-worker-killer/blob/master/LICENSE
+ :versions: []
+ :when: 2016-05-02 05:45:38.323867000 Z
+- - :license
+ - json
+ - ruby
+ - :who: Connor Shea
+ :why: https://github.com/flori/json/tree/master#license
+ :versions: []
+ :when: 2016-05-02 05:50:07.826564000 Z
+- - :license
+ - unf
+ - BSD
+ - :who: Connor Shea
+ :why: https://github.com/knu/ruby-unf/blob/master/LICENSE
+ :versions: []
+ :when: 2016-05-02 05:51:46.886872000 Z
+- - :license
+ - rubypants
+ - BSD
+ - :who: Connor Shea
+ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
+ :versions: []
+ :when: 2016-05-02 05:56:50.696858000 Z
+- - :whitelist
+ - LGPLv2+
+ - :who: Stan Hu
+ :why: Equivalent to LGPLv2
+ :versions: []
+ :when: 2016-06-07 17:14:10.907682000 Z
+- - :whitelist
+ - Artistic 2.0
+ - :who: Josh Frye
+ :why: Disk/mount information display on Admin pages
+ :versions: []
+ :when: 2016-06-29 16:32:45.432113000 Z
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 4f39016bfa4..45a8c1add3e 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -39,6 +39,10 @@ Rails.application.configure do
config.action_mailer.delivery_method = :letter_opener_web
# Don't make a mess when bootstrapping a development environment
config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
+ config.action_mailer.preview_path = 'spec/mailers/previews'
config.eager_load = false
+
+ # Do not log asset requests
+ config.assets.quiet = true
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index a703c0934f7..fb25d3a8b14 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -20,7 +20,7 @@ Rails.application.configure do
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment
- config.action_controller.allow_forgery_protection = false
+ config.action_controller.allow_forgery_protection = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
diff --git a/config/gitlab.teatro.yml b/config/gitlab.teatro.yml
index 01c8dc5ff98..75b79b837e0 100644
--- a/config/gitlab.teatro.yml
+++ b/config/gitlab.teatro.yml
@@ -47,11 +47,13 @@ production: &base
backup:
path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/)
+ repositories:
+ storages: # REPO PATHS MUST NOT BE A SYMLINK!!!
+ default: /apps/repositories/
+
gitlab_shell:
path: /apps/gitlab-shell/
- # REPOS_PATH MUST NOT BE A SYMLINK!!!
- repos_path: /apps/repositories/
hooks_path: /apps/gitlab-shell/hooks/
upload_pack: true
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e682bcb976d..325eca72862 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -98,6 +98,7 @@ production: &base
wiki: true
snippets: false
builds: true
+ container_registry: true
## Webhook settings
# Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10)
@@ -163,6 +164,9 @@ production: &base
# Flag stuck CI builds as failed
stuck_ci_builds_worker:
cron: "0 0 * * *"
+ # Remove expired build artifacts
+ expire_build_artifacts_worker:
+ cron: "50 * * * *"
# Periodically run 'git fsck' on all repositories. If started more than
# once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker:
@@ -175,6 +179,15 @@ production: &base
repository_archive_cache_worker:
cron: "0 * * * *"
+ registry:
+ # enabled: true
+ # host: registry.example.com
+ # port: 5005
+ # api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API
+ # key: config/registry.key
+ # path: shared/registry
+ # issuer: gitlab-issuer
+
#
# 2. GitLab CI settings
# ==========================
@@ -415,6 +428,13 @@ production: &base
satellites:
path: /home/git/gitlab-satellites/
+ ## Repositories settings
+ repositories:
+ # Paths where repositories can be stored. Give the canonicalized absolute pathname.
+ # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
+ storages: # You must have at least a `default` storage path.
+ default: /home/git/repositories/
+
## Backup settings
backup:
path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/)
@@ -439,9 +459,6 @@ production: &base
## GitLab Shell settings
gitlab_shell:
path: /home/git/gitlab-shell/
-
- # REPOS_PATH MUST NOT BE A SYMLINK!!!
- repos_path: /home/git/repositories/
hooks_path: /home/git/gitlab-shell/hooks/
# File that contains the secret key for verifying access for gitlab-shell.
@@ -515,11 +532,13 @@ test:
# user: YOUR_USERNAME
satellites:
path: tmp/tests/gitlab-satellites/
+ repositories:
+ storages:
+ default: tmp/tests/repositories/
backup:
path: tmp/tests/backups
gitlab_shell:
path: tmp/tests/gitlab-shell/
- repos_path: tmp/tests/repositories/
hooks_path: tmp/tests/gitlab-shell/hooks/
issues_tracker:
redmine:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 23c8cea038a..51d93e8cde0 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -52,7 +52,7 @@ class Settings < Settingslogic
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
- if !current.nil?
+ unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
@@ -96,7 +96,6 @@ class Settings < Settingslogic
end
end
-
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil?
@@ -124,9 +123,8 @@ if Settings.ldap['enabled'] || Rails.env.test?
end
end
-
Settings['omniauth'] ||= Settingslogic.new({})
-Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil?
+Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil?
Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil?
Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil?
Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil?
@@ -134,7 +132,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block
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['providers'] ||= []
Settings.omniauth['cas3'] ||= Settingslogic.new({})
Settings.omniauth.cas3['session_duration'] ||= 8.hours
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
@@ -168,7 +166,7 @@ end
Settings['shared'] ||= Settingslogic.new({})
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
-Settings['issues_tracker'] ||= {}
+Settings['issues_tracker'] ||= {}
#
# GitLab
@@ -183,7 +181,7 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80
Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || ''
-Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
+Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
@@ -196,7 +194,7 @@ Settings.gitlab['user_home'] ||= begin
rescue ArgumentError # no user configured
'/home/' + Settings.gitlab['user']
end
-Settings.gitlab['time_zone'] ||= nil
+Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
@@ -206,18 +204,18 @@ Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
Settings.gitlab['session_expire_delay'] ||= 10080
-Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
-Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
-Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
-Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
-Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
-Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
+Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
+Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
+Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
+Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
+Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
+Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
+Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
-
#
# CI
#
@@ -225,8 +223,8 @@ Settings['gitlab_ci'] ||= Settingslogic.new({})
Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil?
Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
-Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
+Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
#
# Reply by email
@@ -240,7 +238,20 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled']
Settings['artifacts'] ||= Settingslogic.new({})
Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil?
Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root)
-Settings.artifacts['max_size'] ||= 100 # in megabytes
+Settings.artifacts['max_size'] ||= 100 # in megabytes
+
+#
+# Registry
+#
+Settings['registry'] ||= Settingslogic.new({})
+Settings.registry['enabled'] ||= false
+Settings.registry['host'] ||= "example.com"
+Settings.registry['port'] ||= nil
+Settings.registry['api_url'] ||= "http://localhost:5000/"
+Settings.registry['key'] ||= nil
+Settings.registry['issuer'] ||= nil
+Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':')
+Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
#
# Git LFS
@@ -265,6 +276,9 @@ Settings['cron_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
+Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
+Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker'
@@ -274,6 +288,9 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
+Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker'
#
# GitLab Shell
@@ -284,7 +301,6 @@ Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitla
Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret')
Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil?
Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil?
-Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/'
Settings.gitlab_shell['ssh_host'] ||= Settings.gitlab.ssh_host
Settings.gitlab_shell['ssh_port'] ||= 22
Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user
@@ -292,13 +308,21 @@ Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user
Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_ssh_path_prefix)
#
+# Repositories
+#
+Settings['repositories'] ||= Settingslogic.new({})
+Settings.repositories['storages'] ||= {}
+# Setting gitlab_shell.repos_path is DEPRECATED and WILL BE REMOVED in version 9.0
+Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/'
+
+#
# Backup
#
Settings['backup'] ||= Settingslogic.new({})
Settings.backup['keep_time'] ||= 0
Settings.backup['pg_schema'] = nil
Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root)
-Settings.backup['archive_permissions'] ||= 0600
+Settings.backup['archive_permissions'] ||= 0600
Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil })
# Convert upload connection settings to use symbol keys, to make Fog happy
if Settings.backup['upload']['connection']
@@ -321,7 +345,6 @@ Settings.git['timeout'] ||= 10
Settings['satellites'] ||= Settingslogic.new({})
Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root)
-
#
# Extra customization
#
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
new file mode 100644
index 00000000000..3ba9e36c567
--- /dev/null
+++ b/config/initializers/6_validations.rb
@@ -0,0 +1,24 @@
+def storage_name_valid?(name)
+ !!(name =~ /\A[a-zA-Z0-9\-_]+\z/)
+end
+
+def find_parent_path(name, path)
+ Gitlab.config.repositories.storages.detect do |n, p|
+ name != n && path.chomp('/').start_with?(p.chomp('/'))
+ end
+end
+
+def error(message)
+ raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
+end
+
+error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
+
+Gitlab.config.repositories.storages.each do |name, path|
+ error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
+
+ parent_name, _parent_path = find_parent_path(name, path)
+ if parent_name
+ error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
+ end
+end
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index df28d30d750..1933afcbfb1 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -2,7 +2,7 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
aws_file = Rails.root.join('config', 'aws.yml')
-if File.exists?(aws_file)
+if File.exist?(aws_file)
AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env]
CarrierWave.configure do |config|
@@ -20,7 +20,7 @@ if File.exists?(aws_file)
config.fog_public = false
# optional, defaults to {}
- config.fog_attributes = { 'Cache-Control'=>'max-age=315576000' }
+ config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' }
# optional time (in seconds) that authenticated urls will be valid.
# when fog_public is false and provider is AWS or Google, defaults to 600
diff --git a/config/initializers/chronic_duration.rb b/config/initializers/chronic_duration.rb
new file mode 100644
index 00000000000..b65b06c813a
--- /dev/null
+++ b/config/initializers/chronic_duration.rb
@@ -0,0 +1 @@
+ChronicDuration.raise_exceptions = true
diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb
index 8fd27b1d88e..de2cdc6ecae 100644
--- a/config/initializers/default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -9,3 +9,4 @@ unless Gitlab.config.gitlab_on_standard_port?
end
Rails.application.routes.default_url_options = default_url_options
+ActionMailer::Base.asset_host = Settings.gitlab['base_url']
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 31dceaebcad..73977341b73 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -212,7 +212,7 @@ Devise.setup do |config|
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
if server['allow_username_or_email_login']
- email_stripping_proc = ->(name) {name.gsub(/@.*\z/,'')}
+ email_stripping_proc = ->(name) {name.gsub(/@.*\z/, '')}
else
email_stripping_proc = ->(name) {name}
end
@@ -243,7 +243,7 @@ Devise.setup do |config|
when Hash
# Add procs for handling SLO
if provider['name'] == 'cas3'
- provider['args'][:on_single_sign_out] = lambda do |request|
+ provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index]
raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket)
Gitlab::OAuth::Session.destroy(:cas3, ticket)
diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb
deleted file mode 100644
index 05a1852cdbd..00000000000
--- a/config/initializers/devise_async.rb
+++ /dev/null
@@ -1 +0,0 @@
-Devise::Async.backend = :sidekiq
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 66ac88e9f4a..618dba74151 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -12,7 +12,7 @@ Doorkeeper.configure do
end
resource_owner_from_credentials do |routes|
- Gitlab::Auth.new.find(params[:username], params[:password])
+ Gitlab::Auth.find_with_user_password(params[:username], params[:password])
end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
@@ -52,7 +52,7 @@ Doorkeeper.configure do
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
default_scopes :api
- #optional_scopes :write, :update
+ # optional_scopes :write, :update
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
@@ -71,7 +71,7 @@ Doorkeeper.configure do
# The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
# (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
#
- native_redirect_uri nil#'urn:ietf:wg:oauth:2.0:oob'
+ native_redirect_uri nil # 'urn:ietf:wg:oauth:2.0:oob'
# Specify what grant flows are enabled in array of Strings. The valid
# strings and the flows they enable are:
diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb
index 751fccead07..7454c33c9dd 100644
--- a/config/initializers/gitlab_shell_secret_token.rb
+++ b/config/initializers/gitlab_shell_secret_token.rb
@@ -1,19 +1 @@
-# Be sure to restart your server when you modify this file.
-
-require 'securerandom'
-
-# Your secret key for verifying the gitlab_shell.
-
-
-secret_file = Gitlab.config.gitlab_shell.secret_file
-
-unless File.exist? secret_file
- # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
- token = SecureRandom.hex(16)
- File.write(secret_file, token)
-end
-
-link_path = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret')
-if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(link_path)
- FileUtils.symlink(secret_file, link_path)
-end
+Gitlab::Shell.new.generate_and_link_secret_token
diff --git a/config/initializers/haml.rb b/config/initializers/haml.rb
deleted file mode 100644
index 1516476815a..00000000000
--- a/config/initializers/haml.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-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/hamlit.rb b/config/initializers/hamlit.rb
new file mode 100644
index 00000000000..7b545d8c06c
--- /dev/null
+++ b/config/initializers/hamlit.rb
@@ -0,0 +1,18 @@
+module Hamlit
+ class TemplateHandler
+ def call(template)
+ Engine.new(
+ generator: Temple::Generators::RailsOutputBuffer,
+ attr_quote: '"',
+ ).call(template.source)
+ end
+ end
+end
+
+ActionView::Template.register_template_handler(
+ :haml,
+ Hamlit::TemplateHandler.new,
+)
+
+Hamlit::Filters.remove_filter('coffee')
+Hamlit::Filters.remove_filter('coffeescript')
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
new file mode 100644
index 00000000000..6796407d4e6
--- /dev/null
+++ b/config/initializers/health_check.rb
@@ -0,0 +1,17 @@
+# Email forcibly included in the standard checks, but the email health check
+# doesn't support the full range of SMTP options, which can result in failures
+# for valid SMTP configurations.
+# Overwrite the HealthCheck's detection of whether email is configured
+# in order to avoid the email check during standard checks
+module HealthCheck
+ class Utils
+ def self.mailer_configured?
+ false
+ end
+ end
+end
+
+HealthCheck.setup do |config|
+ config.standard_checks = ['database', 'migrations', 'cache']
+ config.full_checks = ['database', 'migrations', 'cache']
+end
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 9e8b0131f8f..3d1a41a4652 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,3 +8,7 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
+#
+ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable %w(award_emoji)
+end
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index b2d08d87bac..c4266ab8ba5 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -12,6 +12,7 @@ if Gitlab::Metrics.enabled?
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Metrics::RackMiddleware)
+ config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
end
Sidekiq.configure_server do |config|
@@ -95,18 +96,27 @@ if Gitlab::Metrics.enabled?
config.instrument_instance_methods(const)
end
- # Instruments all Banzai filters
- Dir[Rails.root.join('lib', 'banzai', 'filter', '*.rb')].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai::Filter.const_get(klass)
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
+ config.instrument_methods(const)
+ config.instrument_instance_methods(const)
+ end
end
config.instrument_methods(Banzai::Renderer)
config.instrument_methods(Banzai::Querying)
+ config.instrument_instance_methods(Banzai::ObjectRenderer)
+ config.instrument_instance_methods(Banzai::Redactor)
+ config.instrument_methods(Banzai::NoteRenderer)
+
[Issuable, Mentionable, Participable].each do |klass|
config.instrument_instance_methods(klass)
config.instrument_instance_methods(klass::ClassMethods)
@@ -118,9 +128,34 @@ if Gitlab::Metrics.enabled?
# Instrument the classes used for checking if somebody has push access.
config.instrument_instance_methods(Gitlab::GitAccess)
config.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ config.instrument_instance_methods(API::Helpers)
+
+ config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ config.instrument_methods(Rinku)
end
GC::Profiler.enable
Gitlab::Metrics::Sampler.new.start
+
+ module TrackNewRedisConnections
+ def connect(*args)
+ val = super
+
+ if current_transaction = Gitlab::Metrics::Transaction.current
+ current_transaction.increment(:new_redis_connections, 1)
+ end
+
+ val
+ end
+ end
+
+ class ::Redis::Client
+ prepend TrackNewRedisConnections
+ end
end
diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb
deleted file mode 100644
index 62b05a55285..00000000000
--- a/config/initializers/monkey_patch.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-## 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/omniauth.rb b/config/initializers/omniauth.rb
index 4c164119fff..26c30e523a7 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -13,7 +13,7 @@ end
OmniAuth.config.full_host = Settings.gitlab['base_url']
OmniAuth.config.allowed_request_methods = [:post]
-#In case of auto sign-in, the GET method is used (users don't get to click on a button)
+# In case of auto sign-in, the GET method is used (users don't get to click on a button)
OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present?
OmniAuth.config.before_request_phase do |env|
OmniAuth::RequestForgeryProtection.call(env)
diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb
index b9176688bc4..cb00d3cfe95 100644
--- a/config/initializers/premailer.rb
+++ b/config/initializers/premailer.rb
@@ -3,6 +3,6 @@ Premailer::Rails.config.merge!(
generate_text_part: false,
preserve_styles: true,
remove_comments: true,
- remove_ids: true,
+ remove_ids: false,
remove_scripts: false
)
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
index 30d05f16153..69052c029f2 100644
--- a/config/initializers/rack_attack.rb.example
+++ b/config/initializers/rack_attack.rb.example
@@ -10,7 +10,8 @@ paths_to_be_protected = [
"#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session",
"#{Rails.application.config.relative_url_root}/users",
"#{Rails.application.config.relative_url_root}/users/confirmation",
- "#{Rails.application.config.relative_url_root}/unsubscribes/"
+ "#{Rails.application.config.relative_url_root}/unsubscribes/",
+ "#{Rails.application.config.relative_url_root}/import/github/personal_access_token"
]
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 599dabb9e50..0d9d87bac00 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -23,6 +23,6 @@ else
secure: Gitlab.config.gitlab.https,
httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60,
- path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
+ path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
)
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f1eec674888..593c14a289f 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -13,7 +13,7 @@ Sidekiq.configure_server do |config|
# UGLY Hack to get nested hash from settingslogic
cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
# UGLY hack: Settingslogic doesn't allow 'class' key
- cron_jobs.each { |k,v| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') }
+ cron_jobs.each { |k, v| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') }
Sidekiq::Cron::Job.load_from_hash! cron_jobs
# Database pool should be at least `sidekiq_concurrency` + 2
@@ -23,6 +23,10 @@ Sidekiq.configure_server do |config|
config['pool'] = Sidekiq.options[:concurrency] + 2
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
+
+ # Avoid autoload issue such as 'Mail::Parsers::AddressStruct'
+ # https://github.com/mikel/mail/issues/912#issuecomment-214850355
+ Mail.eager_autoload!
end
Sidekiq.configure_client do |config|
diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample
index 2287a76fca7..bd37080b1c8 100644
--- a/config/initializers/smtp_settings.rb.sample
+++ b/config/initializers/smtp_settings.rb.sample
@@ -10,6 +10,7 @@
if Rails.env.production?
Rails.application.config.action_mailer.delivery_method = :smtp
+ ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
address: "email.server.com",
port: 465,
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index d256a16d42b..df4a933e22f 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -1,3 +1,16 @@
+# Override Rack::Request to make use of the same list of trusted_proxies
+# as the ActionDispatch::Request object. This is necessary for libraries
+# like rack_attack where they don't use ActionDispatch, and we want them
+# to block/throttle requests on private networks.
+# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145
+module Rack
+ class Request
+ def trusted_proxy?(ip)
+ Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip }
+ end
+ end
+end
+
Rails.application.config.action_dispatch.trusted_proxies = (
[ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies)
).map { |proxy| IPAddr.new(proxy) }
diff --git a/config/license_finder.yml b/config/license_finder.yml
new file mode 100644
index 00000000000..e01ebec3298
--- /dev/null
+++ b/config/license_finder.yml
@@ -0,0 +1,2 @@
+---
+decisions_file: './config/dependency_decisions.yml'
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 761a32adb9e..7cab24b295e 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -2,7 +2,7 @@
<%
require "yaml"
require "json"
-require_relative "lib/gitlab/redis"
+require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis)
rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
diff --git a/config/routes.rb b/config/routes.rb
index dafecc94648..18a4ead2b37 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -30,6 +30,11 @@ Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
end
+ concern :access_requestable do
+ post :request_access, on: :collection
+ post :approve_access_request, on: :member
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -56,6 +61,7 @@ Rails.application.routes.draw do
# Autocomplete
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
+ get '/autocomplete/projects' => 'autocomplete#projects'
# Emojis
resources :emojis, only: :index
@@ -64,6 +70,9 @@ Rails.application.routes.draw do
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
+ # JSON Web Token
+ get 'jwt/auth' => 'jwt#auth'
+
# API
API::API.logger Rails.logger
mount API::API => '/api'
@@ -73,14 +82,17 @@ Rails.application.routes.draw do
mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
end
- # Enable Grack support
- mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
+ # Health check
+ get 'health_check(/:checks)' => 'health_check#index', as: :health_check
+
+ # Enable Grack support (for LFS only)
+ mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put]
# Help
get 'help' => 'help#index'
get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ }
get 'help/shortcuts'
- get 'help/ui' => 'help#ui'
+ get 'help/ui' => 'help#ui'
#
# Global snippets
@@ -91,7 +103,8 @@ Rails.application.routes.draw do
end
end
- get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ }
+ get '/s/:username', to: redirect('/u/%{username}/snippets'),
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
#
# Invites
@@ -110,14 +123,22 @@ Rails.application.routes.draw do
end
end
+ #
# Spam reports
+ #
resources :abuse_reports, only: [:new, :create]
#
+ # Notification settings
+ #
+ resources :notification_settings, only: [:create, :update]
+
+ #
# Import
#
namespace :import do
resource :github, only: [:create, :new], controller: :github do
+ post :personal_access_token
get :status
get :callback
get :jobs
@@ -158,6 +179,10 @@ Rails.application.routes.draw do
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
+
+ resource :gitlab_project, only: [:create, :new] do
+ post :create
+ end
end
#
@@ -253,7 +278,9 @@ Rails.application.routes.draw do
end
resource :logs, only: [:show]
+ resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
+ resource :system_info, controller: 'system_info', only: [:show]
resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
root to: 'projects#index', as: :projects
@@ -269,7 +296,7 @@ Rails.application.routes.draw do
post :repository_check
end
- resources :runner_projects
+ resources :runner_projects, only: [:create, :destroy]
end
end
@@ -284,6 +311,7 @@ Rails.application.routes.draw do
resource :application_settings, only: [:show, :update] do
resources :services
put :reset_runners_token
+ put :reset_health_check_token
put :clear_repository_check_states
end
@@ -333,8 +361,16 @@ Rails.application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
- resource :two_factor_auth, only: [:new, :create, :destroy] do
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
+ resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
+ post :create_u2f
post :codes
patch :skip
end
@@ -342,23 +378,18 @@ Rails.application.routes.draw do
end
end
- get 'u/:username/calendar' => 'users#calendar', as: :user_calendar,
- constraints: { username: /.*/ }
-
- 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)/ }
+ scope(path: 'u/:username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get :calendar
+ get :calendar_activities
+ get :groups
+ get :projects
+ get :contributed, as: :contributed_projects
+ get :snippets
+ get '/', action: :show
+ end
#
# Dashboard Area
@@ -403,20 +434,23 @@ Rails.application.routes.draw do
end
scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy] do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- resource :notification_setting, only: [:update]
end
end
resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
- devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations , passwords: :passwords, sessions: :sessions, confirmations: :confirmations }
+ devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
+ registrations: :registrations,
+ passwords: :passwords,
+ sessions: :sessions,
+ confirmations: :confirmations }
devise_scope :user do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
@@ -439,11 +473,39 @@ Rails.application.routes.draw do
post :housekeeping
post :toggle_star
post :markdown_preview
+ post :export
+ post :remove_export
+ post :generate_new_export
+ get :download_export
get :autocomplete_sources
get :activity
+ get :refs
end
scope module: :projects do
+ # Git HTTP clients ('git clone' etc.)
+ scope constraints: { id: /.+\.git/, format: nil } do
+ get '/info/refs', to: 'git_http#info_refs'
+ post '/git-upload-pack', to: 'git_http#git_upload_pack'
+ post '/git-receive-pack', to: 'git_http#git_receive_pack'
+ end
+
+ # Allow /info/refs, /info/refs?service=git-upload-pack, and
+ # /info/refs?service=git-receive-pack, but nothing else.
+ #
+ git_http_handshake = lambda do |request|
+ request.query_string.blank? ||
+ request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
+ end
+
+ ref_redirect = redirect do |params, request|
+ path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+ path << "?#{request.query_string}" unless request.query_string.blank?
+ path
+ end
+
+ get '/info/refs', constraints: git_http_handshake, to: ref_redirect
+
# Blob routes:
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
@@ -582,7 +644,6 @@ Rails.application.routes.draw do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
- post '/wikis/markdown_preview', to:'wikis#markdown_preview'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
@@ -591,6 +652,7 @@ Rails.application.routes.draw do
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
+ post '/wikis/*id/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview'
end
resource :repository, only: [:show, :create] do
@@ -614,7 +676,6 @@ Rails.application.routes.draw do
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
- resource :notification_setting, only: [:update]
resources :refs, only: [] do
collection do
@@ -643,6 +704,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
+ post :toggle_award_emoji
post :remove_wip
end
@@ -658,10 +720,19 @@ Rails.application.routes.draw do
resource :release, only: [:edit, :update]
end
- resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resource :variables, only: [:show, :update]
+ resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
+ resources :pipelines, only: [:index, :new, :create, :show] do
+ member do
+ post :cancel
+ post :retry
+ end
+ end
+
+ resources :environments, only: [:index, :show, :new, :create, :destroy]
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -672,6 +743,7 @@ Rails.application.routes.draw do
post :cancel
post :retry
post :erase
+ get :trace
get :raw
end
@@ -679,6 +751,7 @@ Rails.application.routes.draw do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
+ post :keep
end
end
@@ -688,6 +761,8 @@ Rails.application.routes.draw do
end
end
+ resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+
resources :milestones, constraints: { id: /\d+/ } do
member do
put :sort_issues
@@ -698,16 +773,19 @@ Rails.application.routes.draw do
resources :labels, constraints: { id: /\d+/ } do
collection do
post :generate
+ post :set_priorities
end
member do
post :toggle_subscription
+ delete :remove_priority
end
end
resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
+ post :toggle_award_emoji
get :referenced_merge_requests
get :related_branches
get :can_create_branch
@@ -717,7 +795,7 @@ Rails.application.routes.draw do
end
end
- resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
+ resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
@@ -736,14 +814,13 @@ Rails.application.routes.draw do
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
+ post :toggle_award_emoji
delete :delete_attachment
end
-
- collection do
- post :award_toggle
- end
end
+ resources :todos, only: [:create]
+
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
@@ -774,7 +851,7 @@ Rails.application.routes.draw do
end
# Get all keys of user
- get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
+ get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
end
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
index b99d24a03c9..51ff451eb4c 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_builds.rb
@@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds
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, 'master')
+ @project.ensure_pipeline(sha, 'master')
end
rescue
[]
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
new file mode 100644
index 00000000000..baac32f2d10
--- /dev/null
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -0,0 +1,33 @@
+Gitlab::Seeder.quiet do
+ emoji = Gitlab::AwardEmoji.emojis.keys
+
+ Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
+ project = issue.project
+
+ project.team.users.sample(2).each do |user|
+ issue.create_award_emoji(emoji.sample, user)
+
+ issue.notes.sample(2).each do |note|
+ next if note.system?
+ note.create_award_emoji(emoji.sample, user)
+ end
+
+ print '.'
+ end
+ end
+
+ MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr|
+ project = mr.project
+
+ project.team.users.sample(2).each do |user|
+ mr.create_award_emoji(emoji.sample, user)
+
+ mr.notes.sample(2).each do |note|
+ next if note.system?
+ note.create_award_emoji(emoji.sample, user)
+ end
+
+ print '.'
+ end
+ end
+end
diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb
index 78746c83225..b37dc794015 100644
--- a/db/fixtures/production/001_admin.rb
+++ b/db/fixtures/production/001_admin.rb
@@ -16,21 +16,21 @@ user = User.new(user_args)
user.skip_confirmation!
if user.save
- puts "Administrator account created:".green
+ puts "Administrator account created:".color(:green)
puts
- puts "login: root".green
+ puts "login: root".color(:green)
if user_args.key?(:password)
- puts "password: #{user_args[:password]}".green
+ puts "password: #{user_args[:password]}".color(:green)
else
- puts "password: You'll be prompted to create one on your first visit.".green
+ puts "password: You'll be prompted to create one on your first visit.".color(:green)
end
puts
else
- puts "Could not create the default administrator account:".red
+ puts "Could not create the default administrator account:".color(:red)
puts
user.errors.full_messages.map do |message|
- puts "--> #{message}".red
+ puts "--> #{message}".color(:red)
end
puts
diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb
index d7644b6847a..f93dc92b70f 100644
--- a/db/migrate/20121220064453_init_schema.rb
+++ b/db/migrate/20121220064453_init_schema.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class InitSchema < ActiveRecord::Migration
def up
diff --git a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
index d0fca269871..84fd2060770 100644
--- a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
+++ b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RenameOwnerToCreatorForProject < ActiveRecord::Migration
def change
rename_column :projects, :owner_id, :creator_id
diff --git a/db/migrate/20130110172407_add_public_to_project.rb b/db/migrate/20130110172407_add_public_to_project.rb
index 45edba48152..4362aadcc1d 100644
--- a/db/migrate/20130110172407_add_public_to_project.rb
+++ b/db/migrate/20130110172407_add_public_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPublicToProject < ActiveRecord::Migration
def change
add_column :projects, :public, :boolean, default: false, null: false
diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb
index 288d0f07c9a..ba8c50b53e2 100644
--- a/db/migrate/20130123114545_add_issues_tracker_to_project.rb
+++ b/db/migrate/20130123114545_add_issues_tracker_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIssuesTrackerToProject < ActiveRecord::Migration
def change
add_column :projects, :issues_tracker, :string, default: :gitlab, null: false
diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb
index 38b5f439a2d..1350eadb60e 100644
--- a/db/migrate/20130125090214_add_user_permissions.rb
+++ b/db/migrate/20130125090214_add_user_permissions.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUserPermissions < ActiveRecord::Migration
def up
add_column :users, :can_create_group, :boolean, default: true, null: false
diff --git a/db/migrate/20130131070232_remove_private_flag_from_project.rb b/db/migrate/20130131070232_remove_private_flag_from_project.rb
index 5754db11558..f0273ba448e 100644
--- a/db/migrate/20130131070232_remove_private_flag_from_project.rb
+++ b/db/migrate/20130131070232_remove_private_flag_from_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemovePrivateFlagFromProject < ActiveRecord::Migration
def up
remove_column :projects, :private_flag
diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb
index ef02e489d03..62676ce8914 100644
--- a/db/migrate/20130206084024_add_description_to_namsespace.rb
+++ b/db/migrate/20130206084024_add_description_to_namsespace.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDescriptionToNamsespace < ActiveRecord::Migration
def change
add_column :namespaces, :description, :string, default: '', null: false
diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb
index 6d03777901c..bd9a4767b69 100644
--- a/db/migrate/20130207104426_add_description_to_teams.rb
+++ b/db/migrate/20130207104426_add_description_to_teams.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDescriptionToTeams < ActiveRecord::Migration
def change
add_column :user_teams, :description, :string, default: '', null: false
diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
index 71763d18aee..56b01cbf892 100644
--- a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
+++ b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIssuesTrackerIdToProject < ActiveRecord::Migration
def change
add_column :projects, :issues_tracker_id, :string
diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
index 23797fe1894..4722cc13d4b 100644
--- a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
+++ b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration
def change
rename_column :merge_requests, :state, :merge_status
diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb
index 062103d0e33..3a5e978a182 100644
--- a/db/migrate/20130218140952_add_state_to_issue.rb
+++ b/db/migrate/20130218140952_add_state_to_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStateToIssue < ActiveRecord::Migration
def change
add_column :issues, :state, :string
diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb
index ac4108ee311..e0180c755e2 100644
--- a/db/migrate/20130218141038_add_state_to_merge_request.rb
+++ b/db/migrate/20130218141038_add_state_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStateToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :state, :string
diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb
index c84039106bd..5f71608692c 100644
--- a/db/migrate/20130218141117_add_state_to_milestone.rb
+++ b/db/migrate/20130218141117_add_state_to_milestone.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStateToMilestone < ActiveRecord::Migration
def change
add_column :milestones, :state, :string
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
index 99289166e81..94c0a6845d5 100644
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ConvertClosedToStateInIssue < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
index bd1e016d679..64a9c761352 100644
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
index d1174bc3d98..41508c2dc95 100644
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ConvertClosedToStateInMilestone < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb
index a7bd82f5000..afa5137061e 100644
--- a/db/migrate/20130218141444_remove_merged_from_merge_request.rb
+++ b/db/migrate/20130218141444_remove_merged_from_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveMergedFromMergeRequest < ActiveRecord::Migration
def up
remove_column :merge_requests, :merged
diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb
index 95cc064252b..f250288bc3b 100644
--- a/db/migrate/20130218141507_remove_closed_from_issue.rb
+++ b/db/migrate/20130218141507_remove_closed_from_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveClosedFromIssue < ActiveRecord::Migration
def up
remove_column :issues, :closed
diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb
index 371835938b2..efa12e32636 100644
--- a/db/migrate/20130218141536_remove_closed_from_merge_request.rb
+++ b/db/migrate/20130218141536_remove_closed_from_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveClosedFromMergeRequest < ActiveRecord::Migration
def up
remove_column :merge_requests, :closed
diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb
index e8dae4a19b1..75ac14e43be 100644
--- a/db/migrate/20130218141554_remove_closed_from_milestone.rb
+++ b/db/migrate/20130218141554_remove_closed_from_milestone.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveClosedFromMilestone < ActiveRecord::Migration
def up
remove_column :milestones, :closed
diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
index d78bd0ae923..97615e47c89 100644
--- a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
+++ b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :new_merge_status, :string
diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
index 1c758c56ffe..3b8c3686c55 100644
--- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
+++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration
def up
execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1"
diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
index 9083183beb0..bd25ffbfc99 100644
--- a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
+++ b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration
def up
remove_column :merge_requests, :merge_status
diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
index 3f8f38dc979..f0595720a39 100644
--- a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
+++ b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration
def change
rename_column :merge_requests, :new_merge_status, :merge_status
diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb
index 8154c21065f..4456d022e3f 100644
--- a/db/migrate/20130304104623_add_state_to_user.rb
+++ b/db/migrate/20130304104623_add_state_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStateToUser < ActiveRecord::Migration
def change
add_column :users, :state, :string
diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb
index e8d5257ac96..9afd1093645 100644
--- a/db/migrate/20130304104740_convert_blocked_to_state.rb
+++ b/db/migrate/20130304104740_convert_blocked_to_state.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ConvertBlockedToState < ActiveRecord::Migration
def up
User.transaction do
diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb
index e010474538c..8f5b2c59b43 100644
--- a/db/migrate/20130304105317_remove_blocked_from_user.rb
+++ b/db/migrate/20130304105317_remove_blocked_from_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveBlockedFromUser < ActiveRecord::Migration
def up
remove_column :users, :blocked
diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb
index 56c9a31ee3c..06e28a49d9d 100644
--- a/db/migrate/20130315124931_user_color_scheme.rb
+++ b/db/migrate/20130315124931_user_color_scheme.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class UserColorScheme < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb
index ad0b4434c43..9860b85f504 100644
--- a/db/migrate/20130318212250_add_snippets_to_features.rb
+++ b/db/migrate/20130318212250_add_snippets_to_features.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSnippetsToFeatures < ActiveRecord::Migration
def change
add_column :projects, :snippets_enabled, :boolean, null: false, default: true
diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb
index f91afc26e77..66eb11a4b2b 100644
--- a/db/migrate/20130319214458_create_forked_project_links.rb
+++ b/db/migrate/20130319214458_create_forked_project_links.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateForkedProjectLinks < ActiveRecord::Migration
def change
create_table :forked_project_links do |t|
diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb
index 92f3a5c7011..376f4618d41 100644
--- a/db/migrate/20130323174317_add_private_to_snippets.rb
+++ b/db/migrate/20130323174317_add_private_to_snippets.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPrivateToSnippets < ActiveRecord::Migration
def change
add_column :snippets, :private, :boolean, null: false, default: true
diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb
index 276aab2ca15..097cb9bc7cb 100644
--- a/db/migrate/20130324151736_add_type_to_snippets.rb
+++ b/db/migrate/20130324151736_add_type_to_snippets.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTypeToSnippets < ActiveRecord::Migration
def change
add_column :snippets, :type, :string
diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
index 4c992bac4d1..9256e62086e 100644
--- a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
+++ b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration
def up
change_column :snippets, :project_id, :integer, :null => true
diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb
index 8c05dd2cc71..6e910fd74c7 100644
--- a/db/migrate/20130324203535_add_type_value_for_snippets.rb
+++ b/db/migrate/20130324203535_add_type_value_for_snippets.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTypeValueForSnippets < ActiveRecord::Migration
def up
Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet')
diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb
index 9f466e38c13..1dc58d4bcc8 100644
--- a/db/migrate/20130325173941_add_notification_level_to_user.rb
+++ b/db/migrate/20130325173941_add_notification_level_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotificationLevelToUser < ActiveRecord::Migration
def change
add_column :users, :notification_level, :integer, null: false, default: 1
diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
index d42ef113738..0592181927e 100644
--- a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
+++ b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration
def change
add_index :users, :authentication_token, unique: true
diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb
index 85e31608d79..04a01612c6f 100644
--- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb
+++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLastActivityColumnIntoProject < ActiveRecord::Migration
def up
add_column :projects, :last_activity_at, :datetime
diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb
index 27de5d6bf55..1e072d9c6e1 100644
--- a/db/migrate/20130404164628_add_notification_level_to_user_project.rb
+++ b/db/migrate/20130404164628_add_notification_level_to_user_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotificationLevelToUserProject < ActiveRecord::Migration
def change
add_column :users_projects, :notification_level, :integer, null: false, default: 3
diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb
index 9077aa2473c..5885b1cc375 100644
--- a/db/migrate/20130410175022_remove_wiki_table.rb
+++ b/db/migrate/20130410175022_remove_wiki_table.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveWikiTable < ActiveRecord::Migration
def up
drop_table :wikis
diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb
index 56ea97e8561..ec953986c6a 100644
--- a/db/migrate/20130419190306_allow_merges_for_forks.rb
+++ b/db/migrate/20130419190306_allow_merges_for_forks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AllowMergesForForks < ActiveRecord::Migration
def self.up
add_column :merge_requests, :target_project_id, :integer, :null => true
diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb
index 315e7ca77b3..c9f1ee4e389 100644
--- a/db/migrate/20130506085413_add_type_to_key.rb
+++ b/db/migrate/20130506085413_add_type_to_key.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTypeToKey < ActiveRecord::Migration
def change
add_column :keys, :type, :string
diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb
index 0dc8cdeb07d..7d6662d358a 100644
--- a/db/migrate/20130506090604_create_deploy_keys_projects.rb
+++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateDeployKeysProjects < ActiveRecord::Migration
def change
create_table :deploy_keys_projects do |t|
diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb
index 6b794cfb5c1..53abc4e7b52 100644
--- a/db/migrate/20130506095501_remove_project_id_from_key.rb
+++ b/db/migrate/20130506095501_remove_project_id_from_key.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveProjectIdFromKey < ActiveRecord::Migration
def up
puts 'Migrate deploy keys: '
diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb
index 298e902df2f..9f764a1d050 100644
--- a/db/migrate/20130522141856_add_more_fields_to_service.rb
+++ b/db/migrate/20130522141856_add_more_fields_to_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMoreFieldsToService < ActiveRecord::Migration
def change
add_column :services, :subdomain, :string
diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb
index 1b22a4934f9..27fbf8983ac 100644
--- a/db/migrate/20130528184641_add_system_to_notes.rb
+++ b/db/migrate/20130528184641_add_system_to_notes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSystemToNotes < ActiveRecord::Migration
class Note < ActiveRecord::Base
end
diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb
index f7b4447e43e..f710c79a9a5 100644
--- a/db/migrate/20130611210815_increase_snippet_text_column_size.rb
+++ b/db/migrate/20130611210815_increase_snippet_text_column_size.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class IncreaseSnippetTextColumnSize < ActiveRecord::Migration
def up
# MYSQL LARGETEXT for snippet
diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb
index 3479c8e64d0..47306a370a8 100644
--- a/db/migrate/20130613165816_add_password_expires_at_to_users.rb
+++ b/db/migrate/20130613165816_add_password_expires_at_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPasswordExpiresAtToUsers < ActiveRecord::Migration
def change
add_column :users, :password_expires_at, :datetime
diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb
index 615e96eb156..3138c0f40a7 100644
--- a/db/migrate/20130613173246_add_created_by_id_to_user.rb
+++ b/db/migrate/20130613173246_add_created_by_id_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCreatedByIdToUser < ActiveRecord::Migration
def change
add_column :users, :created_by_id, :integer
diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb
index cc882c3f10a..26dc16e3b43 100644
--- a/db/migrate/20130614132337_add_improted_to_project.rb
+++ b/db/migrate/20130614132337_add_improted_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImprotedToProject < ActiveRecord::Migration
def change
add_column :projects, :imported, :boolean, default: false, null: false
diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb
index 2efc04f1151..45cff93fe4a 100644
--- a/db/migrate/20130617095603_create_users_groups.rb
+++ b/db/migrate/20130617095603_create_users_groups.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateUsersGroups < ActiveRecord::Migration
def change
create_table :users_groups do |t|
diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb
index 8c2e3dfcaca..6fd4941f615 100644
--- a/db/migrate/20130621195223_add_notification_level_to_user_group.rb
+++ b/db/migrate/20130621195223_add_notification_level_to_user_group.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotificationLevelToUserGroup < ActiveRecord::Migration
def change
add_column :users_groups, :notification_level, :integer, null: false, default: 3
diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb
index 9570a7a3f1e..4113217de59 100644
--- a/db/migrate/20130622115340_add_more_db_index.rb
+++ b/db/migrate/20130622115340_add_more_db_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMoreDbIndex < ActiveRecord::Migration
def change
add_index :deploy_keys_projects, :project_id
diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb
index 544a8366727..3e574ea81b9 100644
--- a/db/migrate/20130624162710_add_fingerprint_to_key.rb
+++ b/db/migrate/20130624162710_add_fingerprint_to_key.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddFingerprintToKey < ActiveRecord::Migration
def change
add_column :keys, :fingerprint, :string
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
index 395083f2a03..bd9d40a50db 100644
--- a/db/migrate/20130711063759_create_project_group_links.rb
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateProjectGroupLinks < ActiveRecord::Migration
def change
create_table :project_group_links do |t|
diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb
index 3f9abb975c3..9e2da73b695 100644
--- a/db/migrate/20130804151314_add_st_diff_to_note.rb
+++ b/db/migrate/20130804151314_add_st_diff_to_note.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStDiffToNote < ActiveRecord::Migration
def change
add_column :notes, :st_diff, :text, :null => true
diff --git a/db/migrate/20130809124851_add_permission_check_to_user.rb b/db/migrate/20130809124851_add_permission_check_to_user.rb
index c26157904c7..9f9dea36101 100644
--- a/db/migrate/20130809124851_add_permission_check_to_user.rb
+++ b/db/migrate/20130809124851_add_permission_check_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPermissionCheckToUser < ActiveRecord::Migration
def change
add_column :users, :last_credential_check_at, :datetime
diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb
index 023a48741b2..d2bdfe1894e 100644
--- a/db/migrate/20130812143708_add_import_url_to_project.rb
+++ b/db/migrate/20130812143708_add_import_url_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportUrlToProject < ActiveRecord::Migration
def change
add_column :projects, :import_url, :string
diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
index e55ae38f144..0e0e78b0f0d 100644
--- a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
+++ b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration
def change
add_column :issues, :iid, :integer
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
index 00e3947a6bb..98f3fa87523 100644
--- a/db/migrate/20130820102832_add_access_to_project_group_link.rb
+++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAccessToProjectGroupLink < ActiveRecord::Migration
def change
add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb
index 539c0617eeb..d22e713a7a1 100644
--- a/db/migrate/20130821090530_remove_deprecated_tables.rb
+++ b/db/migrate/20130821090530_remove_deprecated_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveDeprecatedTables < ActiveRecord::Migration
def up
drop_table :user_teams
diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
index 33e5bae5805..e25b8f91662 100644
--- a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
+++ b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddInternalIdsToMilestones < ActiveRecord::Migration
def change
add_column :milestones, :iid, :integer
diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb
index 9bcd0c7ee06..fbac50c8216 100644
--- a/db/migrate/20130909132950_add_description_to_merge_request.rb
+++ b/db/migrate/20130909132950_add_description_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDescriptionToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :description, :text, null: true
diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb
index 8f1992c37ab..2bdd22d5a04 100644
--- a/db/migrate/20130926081215_change_owner_id_for_group.rb
+++ b/db/migrate/20130926081215_change_owner_id_for_group.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ChangeOwnerIdForGroup < ActiveRecord::Migration
def up
change_column :namespaces, :owner_id, :integer, null: true
diff --git a/db/migrate/20131005191208_add_avatar_to_users.rb b/db/migrate/20131005191208_add_avatar_to_users.rb
index 7b4de37ad72..df9057b81d6 100644
--- a/db/migrate/20131005191208_add_avatar_to_users.rb
+++ b/db/migrate/20131005191208_add_avatar_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAvatarToUsers < ActiveRecord::Migration
def change
add_column :users, :avatar, :string
diff --git a/db/migrate/20131009115346_add_confirmable_to_users.rb b/db/migrate/20131009115346_add_confirmable_to_users.rb
index 249cbe704ed..d714dd98e85 100644
--- a/db/migrate/20131009115346_add_confirmable_to_users.rb
+++ b/db/migrate/20131009115346_add_confirmable_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddConfirmableToUsers < ActiveRecord::Migration
def self.up
add_column :users, :confirmation_token, :string
diff --git a/db/migrate/20131106151520_remove_default_branch.rb b/db/migrate/20131106151520_remove_default_branch.rb
index 88a890eb3eb..fd3d1ed7ab3 100644
--- a/db/migrate/20131106151520_remove_default_branch.rb
+++ b/db/migrate/20131106151520_remove_default_branch.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveDefaultBranch < ActiveRecord::Migration
def up
remove_column :projects, :default_branch
diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb
index 147178e9dcf..ce37a8e2708 100644
--- a/db/migrate/20131112114325_create_broadcast_messages.rb
+++ b/db/migrate/20131112114325_create_broadcast_messages.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateBroadcastMessages < ActiveRecord::Migration
def change
create_table :broadcast_messages do |t|
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
index 89421cbedad..5efc17b228e 100644
--- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb
+++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddVisibilityLevelToProjects < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb
index 917e690ba47..e8e6908d137 100644
--- a/db/migrate/20131129154016_add_archived_to_projects.rb
+++ b/db/migrate/20131129154016_add_archived_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddArchivedToProjects < ActiveRecord::Migration
def change
add_column :projects, :archived, :boolean, default: false, null: false
diff --git a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
index 473f355eceb..348a284a53e 100644
--- a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
+++ b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddColorAndFontToBroadcastMessages < ActiveRecord::Migration
def change
add_column :broadcast_messages, :color, :string
diff --git a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
index d29e996852e..99d76611524 100644
--- a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
+++ b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEventFieldsForWebHook < ActiveRecord::Migration
def change
add_column :web_hooks, :push_events, :boolean, default: true, null: false
diff --git a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
index 7cec79e7ee8..4333dc59323 100644
--- a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
+++ b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddHideNoSshKeyToUsers < ActiveRecord::Migration
def change
add_column :users, :hide_no_ssh_key, :boolean, :default => false
diff --git a/db/migrate/20131217102743_add_recipients_to_service.rb b/db/migrate/20131217102743_add_recipients_to_service.rb
index 9695c251352..3c76be0f68d 100644
--- a/db/migrate/20131217102743_add_recipients_to_service.rb
+++ b/db/migrate/20131217102743_add_recipients_to_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRecipientsToService < ActiveRecord::Migration
def change
add_column :services, :recipients, :text
diff --git a/db/migrate/20140116231608_add_website_url_to_users.rb b/db/migrate/20140116231608_add_website_url_to_users.rb
index 0996fdcad73..1c39423562e 100644
--- a/db/migrate/20140116231608_add_website_url_to_users.rb
+++ b/db/migrate/20140116231608_add_website_url_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddWebsiteUrlToUsers < ActiveRecord::Migration
def change
add_column :users, :website_url, :string, {:null => false, :default => ''}
diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb
index f34e30925df..395c3edfc79 100644
--- a/db/migrate/20140122112253_create_merge_request_diffs.rb
+++ b/db/migrate/20140122112253_create_merge_request_diffs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateMergeRequestDiffs < ActiveRecord::Migration
def up
create_table :merge_request_diffs do |t|
diff --git a/db/migrate/20140122114406_migrate_mr_diffs.rb b/db/migrate/20140122114406_migrate_mr_diffs.rb
index 1595e2b6472..429aeb2293f 100644
--- a/db/migrate/20140122114406_migrate_mr_diffs.rb
+++ b/db/migrate/20140122114406_migrate_mr_diffs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateMrDiffs < ActiveRecord::Migration
def self.up
execute "INSERT INTO merge_request_diffs ( merge_request_id, st_commits, st_diffs ) SELECT id, st_commits, st_diffs FROM merge_requests"
diff --git a/db/migrate/20140122122549_remove_m_rdiff_fields.rb b/db/migrate/20140122122549_remove_m_rdiff_fields.rb
index 8f863d85a68..bbf35811b61 100644
--- a/db/migrate/20140122122549_remove_m_rdiff_fields.rb
+++ b/db/migrate/20140122122549_remove_m_rdiff_fields.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveMRdiffFields < ActiveRecord::Migration
def up
remove_column :merge_requests, :st_commits
diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb
index 9523ac722f2..888341b7535 100644
--- a/db/migrate/20140125162722_add_avatar_to_projects.rb
+++ b/db/migrate/20140125162722_add_avatar_to_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAvatarToProjects < ActiveRecord::Migration
def change
add_column :projects, :avatar, :string
diff --git a/db/migrate/20140127170938_add_group_avatars.rb b/db/migrate/20140127170938_add_group_avatars.rb
index 2911096dd5d..95d1c1c6b27 100644
--- a/db/migrate/20140127170938_add_group_avatars.rb
+++ b/db/migrate/20140127170938_add_group_avatars.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddGroupAvatars < ActiveRecord::Migration
def change
add_column :namespaces, :avatar, :string
diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb
index cb78c4af11b..571beb19cdd 100644
--- a/db/migrate/20140209025651_create_emails.rb
+++ b/db/migrate/20140209025651_create_emails.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateEmails < ActiveRecord::Migration
def change
create_table :emails do |t|
diff --git a/db/migrate/20140214102325_add_api_key_to_services.rb b/db/migrate/20140214102325_add_api_key_to_services.rb
index 30eeca2c1f6..b58c36c0a30 100644
--- a/db/migrate/20140214102325_add_api_key_to_services.rb
+++ b/db/migrate/20140214102325_add_api_key_to_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddApiKeyToServices < ActiveRecord::Migration
def change
add_column :services, :api_key, :string
diff --git a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
index 65d28e8cb01..aab8a41c2c3 100644
--- a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
+++ b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexMergeRequestDiffsOnMergeRequestId < ActiveRecord::Migration
def change
add_index :merge_request_diffs, :merge_request_id, unique: true
diff --git a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
index 7017148702a..ec163bb843c 100644
--- a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
+++ b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTagPushHooksToProjectHook < ActiveRecord::Migration
def change
add_column :web_hooks, :tag_push_events, :boolean, default: false
diff --git a/db/migrate/20140312145357_add_import_status_to_project.rb b/db/migrate/20140312145357_add_import_status_to_project.rb
index ef972e8342a..9947cd8c6f9 100644
--- a/db/migrate/20140312145357_add_import_status_to_project.rb
+++ b/db/migrate/20140312145357_add_import_status_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportStatusToProject < ActiveRecord::Migration
def change
add_column :projects, :import_status, :string
diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb
index 0a9f73a5758..f2e91fe1b40 100644
--- a/db/migrate/20140313092127_migrate_already_imported_projects.rb
+++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateAlreadyImportedProjects < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20140407135544_fix_namespaces.rb b/db/migrate/20140407135544_fix_namespaces.rb
index 59665d538f0..91374966698 100644
--- a/db/migrate/20140407135544_fix_namespaces.rb
+++ b/db/migrate/20140407135544_fix_namespaces.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FixNamespaces < ActiveRecord::Migration
def up
Namespace.where('name <> path and type is null').each do |namespace|
diff --git a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
index 1f6d85d5f66..fb9c7a6636e 100644
--- a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
+++ b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration
def up
change_column :merge_request_diffs, :state, :string, null: true,
diff --git a/db/migrate/20140415124820_limits_to_mysql.rb b/db/migrate/20140415124820_limits_to_mysql.rb
index 3f6e62617c5..c712423bcd1 100644
--- a/db/migrate/20140415124820_limits_to_mysql.rb
+++ b/db/migrate/20140415124820_limits_to_mysql.rb
@@ -1 +1,2 @@
+# rubocop:disable all
require_relative 'limits_to_mysql'
diff --git a/db/migrate/20140416074002_add_index_on_iid.rb b/db/migrate/20140416074002_add_index_on_iid.rb
index 85269e2a03b..6cdaa5a3c08 100644
--- a/db/migrate/20140416074002_add_index_on_iid.rb
+++ b/db/migrate/20140416074002_add_index_on_iid.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexOnIid < ActiveRecord::Migration
def change
RemoveDuplicateIid.clean(Issue)
diff --git a/db/migrate/20140416185734_index_on_current_sign_in_at.rb b/db/migrate/20140416185734_index_on_current_sign_in_at.rb
index 0bf80ce154a..8c620b545bd 100644
--- a/db/migrate/20140416185734_index_on_current_sign_in_at.rb
+++ b/db/migrate/20140416185734_index_on_current_sign_in_at.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class IndexOnCurrentSignInAt < ActiveRecord::Migration
def change
add_index :users, :current_sign_in_at
diff --git a/db/migrate/20140428105831_add_notes_index_updated_at.rb b/db/migrate/20140428105831_add_notes_index_updated_at.rb
index 6c25570f128..0589101af93 100644
--- a/db/migrate/20140428105831_add_notes_index_updated_at.rb
+++ b/db/migrate/20140428105831_add_notes_index_updated_at.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotesIndexUpdatedAt < ActiveRecord::Migration
def change
add_index :notes, :updated_at
diff --git a/db/migrate/20140502115131_add_repo_size_to_db.rb b/db/migrate/20140502115131_add_repo_size_to_db.rb
index 7361d1a9440..090b30a4f26 100644
--- a/db/migrate/20140502115131_add_repo_size_to_db.rb
+++ b/db/migrate/20140502115131_add_repo_size_to_db.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRepoSizeToDb < ActiveRecord::Migration
def change
add_column :projects, :repository_size, :float, default: 0
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index efdf53112fd..84463727b3b 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
diff --git a/db/migrate/20140611135229_add_position_to_merge_request.rb b/db/migrate/20140611135229_add_position_to_merge_request.rb
index d5fdecd0c39..3a7d2f7c359 100644
--- a/db/migrate/20140611135229_add_position_to_merge_request.rb
+++ b/db/migrate/20140611135229_add_position_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPositionToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :position, :integer, default: 0
diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb
index 412f0f6f34b..32dd99e83be 100644
--- a/db/migrate/20140625115202_create_users_star_projects.rb
+++ b/db/migrate/20140625115202_create_users_star_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateUsersStarProjects < ActiveRecord::Migration
def change
create_table :users_star_projects do |t|
diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb
index 3a4b6a152dc..df0f8cb9f03 100644
--- a/db/migrate/20140729134820_create_labels.rb
+++ b/db/migrate/20140729134820_create_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateLabels < ActiveRecord::Migration
def change
create_table :labels do |t|
diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb
index 2bfc4ae2094..fa5992605f8 100644
--- a/db/migrate/20140729140420_create_label_links.rb
+++ b/db/migrate/20140729140420_create_label_links.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateLabelLinks < ActiveRecord::Migration
def change
create_table :label_links do |t|
diff --git a/db/migrate/20140729145339_migrate_project_tags.rb b/db/migrate/20140729145339_migrate_project_tags.rb
index 5760e4bfeaa..ac46847f3e6 100644
--- a/db/migrate/20140729145339_migrate_project_tags.rb
+++ b/db/migrate/20140729145339_migrate_project_tags.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateProjectTags < ActiveRecord::Migration
def up
ActsAsTaggableOn::Tagging.where(taggable_type: 'Project', context: 'labels').update_all(context: 'tags')
diff --git a/db/migrate/20140729152420_migrate_taggable_labels.rb b/db/migrate/20140729152420_migrate_taggable_labels.rb
index dc28d727d9a..04cdc6beadd 100644
--- a/db/migrate/20140729152420_migrate_taggable_labels.rb
+++ b/db/migrate/20140729152420_migrate_taggable_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateTaggableLabels < ActiveRecord::Migration
def up
taggings = ActsAsTaggableOn::Tagging.where(taggable_type: ['Issue', 'MergeRequest'], context: 'labels')
diff --git a/db/migrate/20140730111702_add_index_to_labels.rb b/db/migrate/20140730111702_add_index_to_labels.rb
index 494241c873c..cc7ac1fc449 100644
--- a/db/migrate/20140730111702_add_index_to_labels.rb
+++ b/db/migrate/20140730111702_add_index_to_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexToLabels < ActiveRecord::Migration
def change
add_index "labels", :project_id
diff --git a/db/migrate/20140903115954_migrate_to_new_shell.rb b/db/migrate/20140903115954_migrate_to_new_shell.rb
index 54cbe48960a..04acf24284b 100644
--- a/db/migrate/20140903115954_migrate_to_new_shell.rb
+++ b/db/migrate/20140903115954_migrate_to_new_shell.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateToNewShell < ActiveRecord::Migration
def change
return if Rails.env.test?
diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb
index d45a10465be..c2d67fad0ab 100644
--- a/db/migrate/20140907220153_serialize_service_properties.rb
+++ b/db/migrate/20140907220153_serialize_service_properties.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class SerializeServiceProperties < ActiveRecord::Migration
def change
unless column_exists?(:services, :properties)
diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb
index d311f3033ee..bc3c1bb61e4 100644
--- a/db/migrate/20140914113604_add_members_table.rb
+++ b/db/migrate/20140914113604_add_members_table.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMembersTable < ActiveRecord::Migration
def change
create_table :members do |t|
diff --git a/db/migrate/20140914145549_migrate_to_new_members_model.rb b/db/migrate/20140914145549_migrate_to_new_members_model.rb
index 2a5a49c724a..b4c98f016d0 100644
--- a/db/migrate/20140914145549_migrate_to_new_members_model.rb
+++ b/db/migrate/20140914145549_migrate_to_new_members_model.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateToNewMembersModel < ActiveRecord::Migration
def up
execute "INSERT INTO members ( user_id, source_id, source_type, access_level, notification_level, type ) SELECT user_id, group_id, 'Namespace', group_access, notification_level, 'GroupMember' FROM users_groups"
diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb
index 408b9551dbb..aff8e94e5be 100644
--- a/db/migrate/20140914173417_remove_old_member_tables.rb
+++ b/db/migrate/20140914173417_remove_old_member_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveOldMemberTables < ActiveRecord::Migration
def up
drop_table :users_groups
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 5836cd6b8db..8cb120f7007 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration
def change
SlackService.all.each do |slack_service|
diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
index 93826185e8b..688d8578478 100644
--- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
+++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddVisibilityLevelToSnippet < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb
index 07383c6bbc7..3884228456f 100644
--- a/db/migrate/20141118150935_add_audit_event.rb
+++ b/db/migrate/20141118150935_add_audit_event.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAuditEvent < ActiveRecord::Migration
def change
create_table :audit_events do |t|
diff --git a/db/migrate/20141121133009_add_timestamps_to_members.rb b/db/migrate/20141121133009_add_timestamps_to_members.rb
index ef6d4dedf32..68f164cd35d 100644
--- a/db/migrate/20141121133009_add_timestamps_to_members.rb
+++ b/db/migrate/20141121133009_add_timestamps_to_members.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# In 20140914145549_migrate_to_new_members_model.rb we forgot to set the
# created_at and updated_at times for new records in the 'members' table. This
# became a problem after commit c8e78d972a5a628870eefca0f2ccea0199c55bda which
diff --git a/db/migrate/20141121161704_add_identity_table.rb b/db/migrate/20141121161704_add_identity_table.rb
index a85b0426cec..5a399f0d325 100644
--- a/db/migrate/20141121161704_add_identity_table.rb
+++ b/db/migrate/20141121161704_add_identity_table.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIdentityTable < ActiveRecord::Migration
def up
create_table :identities do |t|
diff --git a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb
index 49651c44a82..5aa91c7587a 100644
--- a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb
+++ b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLockedAtToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :locked_at, :datetime
diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb
index af5aa7d8b73..b323ffe96f5 100644
--- a/db/migrate/20141216155758_create_doorkeeper_tables.rb
+++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateDoorkeeperTables < ActiveRecord::Migration
def change
create_table :oauth_applications do |t|
diff --git a/db/migrate/20141217125223_add_owner_to_application.rb b/db/migrate/20141217125223_add_owner_to_application.rb
index 7d5e6d07d0f..e5a669ab4d8 100644
--- a/db/migrate/20141217125223_add_owner_to_application.rb
+++ b/db/migrate/20141217125223_add_owner_to_application.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddOwnerToApplication < ActiveRecord::Migration
def change
add_column :oauth_applications, :owner_id, :integer, null: true
diff --git a/db/migrate/20141223135007_add_import_data_to_project_table.rb b/db/migrate/20141223135007_add_import_data_to_project_table.rb
index 5db78f94cc9..9c8a483e4d5 100644
--- a/db/migrate/20141223135007_add_import_data_to_project_table.rb
+++ b/db/migrate/20141223135007_add_import_data_to_project_table.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportDataToProjectTable < ActiveRecord::Migration
def change
add_column :projects, :import_type, :string
diff --git a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb
index 70e7272f7f3..a18b2f4974d 100644
--- a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb
+++ b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDevelopersCanPushToProtectedBranches < ActiveRecord::Migration
def change
add_column :protected_branches, :developers_can_push, :boolean, default: false, null: false
diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb
index 651e35fdf7a..dfa2f765357 100644
--- a/db/migrate/20150108073740_create_application_settings.rb
+++ b/db/migrate/20150108073740_create_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateApplicationSettings < ActiveRecord::Migration
def change
create_table :application_settings do |t|
diff --git a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb
index aa179ce3a4d..10e6549c729 100644
--- a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb
+++ b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddHomePageUrlForApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :home_page_url, :string
diff --git a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb
index c28ba3197ac..e083973615a 100644
--- a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb
+++ b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddGitlabAccessTokenToUser < ActiveRecord::Migration
def change
add_column :users, :gitlab_access_token, :string
diff --git a/db/migrate/20150125163100_add_default_branch_protection_setting.rb b/db/migrate/20150125163100_add_default_branch_protection_setting.rb
index 5020daf55f3..7ca3116d354 100644
--- a/db/migrate/20150125163100_add_default_branch_protection_setting.rb
+++ b/db/migrate/20150125163100_add_default_branch_protection_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDefaultBranchProtectionSetting < ActiveRecord::Migration
def change
add_column :application_settings, :default_branch_protection, :integer, :default => 2
diff --git a/db/migrate/20150205211843_add_timestamps_to_identities.rb b/db/migrate/20150205211843_add_timestamps_to_identities.rb
index 77cddbfec3b..a78e28eb4eb 100644
--- a/db/migrate/20150205211843_add_timestamps_to_identities.rb
+++ b/db/migrate/20150205211843_add_timestamps_to_identities.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTimestampsToIdentities < ActiveRecord::Migration
def change
add_timestamps(:identities)
diff --git a/db/migrate/20150206181414_add_index_to_created_at.rb b/db/migrate/20150206181414_add_index_to_created_at.rb
index fc624fca60d..a161fad79dc 100644
--- a/db/migrate/20150206181414_add_index_to_created_at.rb
+++ b/db/migrate/20150206181414_add_index_to_created_at.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexToCreatedAt < ActiveRecord::Migration
def change
add_index "users", [:created_at, :id]
diff --git a/db/migrate/20150206222854_add_notification_email_to_user.rb b/db/migrate/20150206222854_add_notification_email_to_user.rb
index ab80f7e582f..ebae092cac8 100644
--- a/db/migrate/20150206222854_add_notification_email_to_user.rb
+++ b/db/migrate/20150206222854_add_notification_email_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotificationEmailToUser < ActiveRecord::Migration
def up
add_column :users, :notification_email, :string
diff --git a/db/migrate/20150209222013_add_missing_index.rb b/db/migrate/20150209222013_add_missing_index.rb
index a816c2e9e8c..18e3ac2cbbb 100644
--- a/db/migrate/20150209222013_add_missing_index.rb
+++ b/db/migrate/20150209222013_add_missing_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMissingIndex < ActiveRecord::Migration
def change
add_index "services", [:created_at, :id]
diff --git a/db/migrate/20150211172122_add_template_to_service.rb b/db/migrate/20150211172122_add_template_to_service.rb
index b1bfbc45ee9..a3e96b25c56 100644
--- a/db/migrate/20150211172122_add_template_to_service.rb
+++ b/db/migrate/20150211172122_add_template_to_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTemplateToService < ActiveRecord::Migration
def change
add_column :services, :template, :boolean, default: false
diff --git a/db/migrate/20150211174341_allow_null_in_services_project_id.rb b/db/migrate/20150211174341_allow_null_in_services_project_id.rb
index 68f02812791..fea95c79adf 100644
--- a/db/migrate/20150211174341_allow_null_in_services_project_id.rb
+++ b/db/migrate/20150211174341_allow_null_in_services_project_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AllowNullInServicesProjectId < ActiveRecord::Migration
def change
change_column :services, :project_id, :integer, null: true
diff --git a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb
index a0439172391..334020376e4 100644
--- a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb
+++ b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTwitterSharingEnabledToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :twitter_sharing_enabled, :boolean, default: true
diff --git a/db/migrate/20150213114800_add_hide_no_password_to_user.rb b/db/migrate/20150213114800_add_hide_no_password_to_user.rb
index 685f0844276..a2af3510b9c 100644
--- a/db/migrate/20150213114800_add_hide_no_password_to_user.rb
+++ b/db/migrate/20150213114800_add_hide_no_password_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddHideNoPasswordToUser < ActiveRecord::Migration
def change
add_column :users, :hide_no_password, :boolean, default: false
diff --git a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb
index c3c7c1ffc77..4e84a13f0d2 100644
--- a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb
+++ b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPasswordAutomaticallySetToUser < ActiveRecord::Migration
def change
add_column :users, :password_automatically_set, :boolean, default: false
diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
index 23ac1b399ec..78e9fd0c3a9 100644
--- a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
+++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration
def change
add_column :users, :bitbucket_access_token, :string
diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb
index cf73a0174f4..560382c3fa1 100644
--- a/db/migrate/20150219004514_add_events_to_services.rb
+++ b/db/migrate/20150219004514_add_events_to_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEventsToServices < ActiveRecord::Migration
def change
add_column :services, :push_events, :boolean, :default => true
diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb
index 3f6d4d83474..300381ad65b 100644
--- a/db/migrate/20150223022001_set_missing_last_activity_at.rb
+++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class SetMissingLastActivityAt < ActiveRecord::Migration
def up
execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL"
diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb
index d54ba9e482f..7843cabc43b 100644
--- a/db/migrate/20150225065047_add_note_events_to_services.rb
+++ b/db/migrate/20150225065047_add_note_events_to_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNoteEventsToServices < ActiveRecord::Migration
def change
add_column :services, :note_events, :boolean, default: true, null: false
diff --git a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb
index 494c3033bff..7d8d65ef2ee 100644
--- a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb
+++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :restricted_visibility_levels, :text
diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb
index 334e5574559..ea53a9d71f2 100644
--- a/db/migrate/20150306023106_fix_namespace_duplication.rb
+++ b/db/migrate/20150306023106_fix_namespace_duplication.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FixNamespaceDuplication < ActiveRecord::Migration
def up
#fixes path duplication
diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb
index 6472138e3ef..f293a9b643f 100644
--- a/db/migrate/20150306023112_add_unique_index_to_namespace.rb
+++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUniqueIndexToNamespace < ActiveRecord::Migration
def change
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb
index e9d42c1e749..5d3dae6e7d8 100644
--- a/db/migrate/20150310194358_add_version_check_to_application_settings.rb
+++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddVersionCheckToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :version_check_enabled, :boolean, default: true
diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb
index a1d4d9dedc5..8adb193b27f 100644
--- a/db/migrate/20150313012111_create_subscriptions_table.rb
+++ b/db/migrate/20150313012111_create_subscriptions_table.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateSubscriptionsTable < ActiveRecord::Migration
def change
create_table :subscriptions do |t|
diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb
index 32731d37d75..df046570361 100644
--- a/db/migrate/20150320234437_add_location_to_user.rb
+++ b/db/migrate/20150320234437_add_location_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLocationToUser < ActiveRecord::Migration
def change
add_column :users, :location, :string
diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb
index 42dc8173e46..9f8b6f4bd59 100644
--- a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb
+++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration
def up
execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1"
diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb
index 6ffbf4cda19..33c20d65e03 100644
--- a/db/migrate/20150327122227_add_public_to_key.rb
+++ b/db/migrate/20150327122227_add_public_to_key.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPublicToKey < ActiveRecord::Migration
def change
add_column :keys, :public, :boolean, default: false, null: false
diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb
index 12c00339eec..67b1554dfd1 100644
--- a/db/migrate/20150327150017_add_import_data_to_project.rb
+++ b/db/migrate/20150327150017_add_import_data_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportDataToProject < ActiveRecord::Migration
def change
add_column :projects, :import_data, :text
diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb
index 11b026ee8f3..eccb0123e77 100644
--- a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb
+++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration
def change
add_column :users, :encrypted_otp_secret, :string
diff --git a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb
index 1d161674a9a..4c56a2fb78b 100644
--- a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb
+++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false
diff --git a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb
index 913958db7c5..fdb6d72917e 100644
--- a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb
+++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
def change
add_column :users, :otp_backup_codes, :text
diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb
index 5d3e856ddce..63d0f184f32 100644
--- a/db/migrate/20150406133311_add_invite_data_to_member.rb
+++ b/db/migrate/20150406133311_add_invite_data_to_member.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddInviteDataToMember < ActiveRecord::Migration
def up
add_column :members, :created_by_id, :integer
diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb
index d9051f9fffd..a10fcc001f4 100644
--- a/db/migrate/20150411000035_fix_identities.rb
+++ b/db/migrate/20150411000035_fix_identities.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FixIdentities < ActiveRecord::Migration
def up
# Up until now, legacy 'ldap' references in the database were charitably
diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb
index 5a0b5d07e50..9f3b25c3971 100644
--- a/db/migrate/20150411180045_rename_buildbox_service.rb
+++ b/db/migrate/20150411180045_rename_buildbox_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RenameBuildboxService < ActiveRecord::Migration
def up
execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';"
diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb
index 700e9f343a6..0fed5eaf461 100644
--- a/db/migrate/20150413192223_add_public_email_to_users.rb
+++ b/db/migrate/20150413192223_add_public_email_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPublicEmailToUsers < ActiveRecord::Migration
def change
add_column :users, :public_email, :string, default: "", null: false
diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb
index c78f5fde85e..fc357cbacc8 100644
--- a/db/migrate/20150417121913_create_project_import_data.rb
+++ b/db/migrate/20150417121913_create_project_import_data.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateProjectImportData < ActiveRecord::Migration
def change
create_table :project_import_data do |t|
diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb
index 46cf63593c9..5a008218fa5 100644
--- a/db/migrate/20150417122318_remove_import_data_from_project.rb
+++ b/db/migrate/20150417122318_remove_import_data_from_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveImportDataFromProject < ActiveRecord::Migration
def up
remove_column :projects, :import_data
diff --git a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb
index 3057ea3c68c..3445e9ce59e 100644
--- a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb
+++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration
include Gitlab::ShellAdapter
diff --git a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb
index 50a9b2439e0..129ce4d04af 100644
--- a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb
+++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration
def up
add_column :application_settings, :default_project_visibility, :integer
diff --git a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
index 281c88d2a7d..8f352414ffd 100644
--- a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
+++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
# It shold be applied before the index additions to ensure that `name` is case sensitive.
diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb
index 13e5038db9c..e77623bf507 100644
--- a/db/migrate/20150425164647_remove_duplicate_tags.rb
+++ b/db/migrate/20150425164647_remove_duplicate_tags.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveDuplicateTags < ActiveRecord::Migration
def up
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
diff --git a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb
index c1b78681519..cbff98cdbc4 100644
--- a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb
+++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration comes from acts_as_taggable_on_engine (originally 2)
class AddMissingUniqueIndices < ActiveRecord::Migration
def self.up
diff --git a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
index 8edb5080781..1568d2dd4ce 100644
--- a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
+++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration comes from acts_as_taggable_on_engine (originally 3)
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
def self.up
diff --git a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb
index 71f2d7f4330..88829b87711 100644
--- a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb
+++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration comes from acts_as_taggable_on_engine (originally 4)
class AddMissingTaggableIndex < ActiveRecord::Migration
def self.up
diff --git a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
index bfb06bc7cda..642c4745321 100644
--- a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
+++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration comes from acts_as_taggable_on_engine (originally 5)
# This migration is added to circumvent issue #623 and have special characters
# work properly
diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb
index 8f1b0cc8935..dd13def4176 100644
--- a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb
+++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration
def up
add_column :application_settings, :default_snippet_visibility, :integer
diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb
index 244637e1c4a..d2c7f3c442e 100644
--- a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb
+++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration
def up
execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\
diff --git a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb
index 184e2653610..b63ea9aec7a 100644
--- a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb
+++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :restricted_signup_domains, :text
diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb
index b02605489be..cd8bf90108d 100644
--- a/db/migrate/20150509180749_convert_legacy_reference_notes.rb
+++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# Convert legacy Markdown-emphasized notes to the current, non-emphasized format
#
# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_
diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb
index 0097587b4f6..bf72e5e2e3a 100644
--- a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb
+++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNoteEventsToWebHooks < ActiveRecord::Migration
def up
add_column :web_hooks, :note_events, :boolean, default: false, null: false
diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb
index 6a78294f0b2..9b02eda56ab 100644
--- a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb
+++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :user_oauth_applications, :bool, default: true
diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb
index 83e08101407..833c36de52d 100644
--- a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb
+++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :after_sign_out_path, :string
diff --git a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb
index 61ff0af41f4..1f5cf1fe5f1 100644
--- a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb
+++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration
def change
unless column_exists?(:application_settings, :session_expire_delay)
diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb
index 2628e450722..df38472f893 100644
--- a/db/migrate/20150610065936_add_dashboard_to_users.rb
+++ b/db/migrate/20150610065936_add_dashboard_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDashboardToUsers < ActiveRecord::Migration
def up
add_column :users, :dashboard, :integer, default: 0
diff --git a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb
index 8eed8678b2f..da0fd457a34 100644
--- a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb
+++ b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDefaultOtpRequiredForLoginValue < ActiveRecord::Migration
def up
execute %q{UPDATE users SET otp_required_for_login = FALSE WHERE otp_required_for_login IS NULL}
diff --git a/db/migrate/20150713160110_add_project_view_to_users.rb b/db/migrate/20150713160110_add_project_view_to_users.rb
index fe3d206df89..0de5a93035c 100644
--- a/db/migrate/20150713160110_add_project_view_to_users.rb
+++ b/db/migrate/20150713160110_add_project_view_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectViewToUsers < ActiveRecord::Migration
def change
add_column :users, :project_view, :integer, default: 0
diff --git a/db/migrate/20150717130904_add_commits_count_to_project.rb b/db/migrate/20150717130904_add_commits_count_to_project.rb
index 9b46daa5933..5799e068c69 100644
--- a/db/migrate/20150717130904_add_commits_count_to_project.rb
+++ b/db/migrate/20150717130904_add_commits_count_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCommitsCountToProject < ActiveRecord::Migration
def change
add_column :projects, :commit_count, :integer, default: 0
diff --git a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb
index 78d45c7f96b..be30e881c74 100644
--- a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb
+++ b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration
def change
add_column :notes, :updated_by_id, :integer
diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb
index e97dc4cf04c..3c749b5d9a9 100644
--- a/db/migrate/20150806104937_create_abuse_reports.rb
+++ b/db/migrate/20150806104937_create_abuse_reports.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateAbuseReports < ActiveRecord::Migration
def change
create_table :abuse_reports do |t|
diff --git a/db/migrate/20150812080800_add_settings_import_sources.rb b/db/migrate/20150812080800_add_settings_import_sources.rb
index 276d2fdb2b1..07f417fa3e3 100644
--- a/db/migrate/20150812080800_add_settings_import_sources.rb
+++ b/db/migrate/20150812080800_add_settings_import_sources.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
require 'yaml'
class AddSettingsImportSources < ActiveRecord::Migration
diff --git a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb
index de2078a9268..7eaa7eda311 100644
--- a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb
+++ b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveOauthTokensFromUsers < ActiveRecord::Migration
def change
remove_column :users, :github_access_token, :string
diff --git a/db/migrate/20150817163600_deduplicate_user_identities.rb b/db/migrate/20150817163600_deduplicate_user_identities.rb
index fceffc48018..b0cfad7d20f 100644
--- a/db/migrate/20150817163600_deduplicate_user_identities.rb
+++ b/db/migrate/20150817163600_deduplicate_user_identities.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class DeduplicateUserIdentities < ActiveRecord::Migration
def change
execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;'
diff --git a/db/migrate/20150818213832_add_sent_notifications.rb b/db/migrate/20150818213832_add_sent_notifications.rb
index 43e8d6a1a82..fa0c3ce0acf 100644
--- a/db/migrate/20150818213832_add_sent_notifications.rb
+++ b/db/migrate/20150818213832_add_sent_notifications.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSentNotifications < ActiveRecord::Migration
def change
create_table :sent_notifications do |t|
diff --git a/db/migrate/20150824002011_add_enable_ssl_verification.rb b/db/migrate/20150824002011_add_enable_ssl_verification.rb
index 093c068fbde..6e992f08834 100644
--- a/db/migrate/20150824002011_add_enable_ssl_verification.rb
+++ b/db/migrate/20150824002011_add_enable_ssl_verification.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEnableSslVerification < ActiveRecord::Migration
def change
add_column :web_hooks, :enable_ssl_verification, :boolean, default: false
diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb
index c4f51363e57..d1f8506d1fe 100644
--- a/db/migrate/20150826001931_add_ci_tables.rb
+++ b/db/migrate/20150826001931_add_ci_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiTables < ActiveRecord::Migration
def change
create_table "ci_application_settings", force: true do |t|
diff --git a/db/migrate/20150902001023_add_template_to_label.rb b/db/migrate/20150902001023_add_template_to_label.rb
index bd381a97b69..0f6ae8d6cc3 100644
--- a/db/migrate/20150902001023_add_template_to_label.rb
+++ b/db/migrate/20150902001023_add_template_to_label.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTemplateToLabel < ActiveRecord::Migration
def change
add_column :labels, :template, :boolean, default: false
diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb
index df3390e8a82..b647bc9c8a2 100644
--- a/db/migrate/20150914215247_add_ci_tags.rb
+++ b/db/migrate/20150914215247_add_ci_tags.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiTags < ActiveRecord::Migration
def change
create_table "ci_taggings", force: true do |t|
diff --git a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb
index 6e924262a13..3f070139418 100644
--- a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb
+++ b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class EnableSslVerificationByDefault < ActiveRecord::Migration
def change
change_column :web_hooks, :enable_ssl_verification, :boolean, default: true
diff --git a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb
index 90ce6c2db3d..ea2ab6e4093 100644
--- a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb
+++ b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class EnableSslVerificationForWebHooks < ActiveRecord::Migration
def up
execute("UPDATE web_hooks SET enable_ssl_verification = true")
diff --git a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb
index 37a27f11935..a504f25b1be 100644
--- a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb
+++ b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddHelpPageTextToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :help_page_text, :text
diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
index 78d9e5f61a1..a18ed93cf37 100644
--- a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
+++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexForCommittedAtAndId < ActiveRecord::Migration
def change
add_index :ci_commits, [:project_id, :committed_at, :id]
diff --git a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb
index 6cf668a170e..c9b6e035122 100644
--- a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb
+++ b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiEnabledToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :ci_enabled, :boolean, null: false, default: true
diff --git a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb
index 0aad6fe5e6e..e1818b566d7 100644
--- a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb
+++ b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveInvalidMilestonesFromMergeRequests < ActiveRecord::Migration
def up
execute("UPDATE merge_requests SET milestone_id = NULL where milestone_id NOT IN (SELECT id FROM milestones)")
diff --git a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb
index c8438b3f6aa..e6975f5b9fe 100644
--- a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb
+++ b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddConsumedTimestepToUsers < ActiveRecord::Migration
def change
add_column :users, :consumed_timestep, :integer
diff --git a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb
index d9af4e71751..1bcb06e4bda 100644
--- a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb
+++ b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLineCodeToSentNotification < ActiveRecord::Migration
def change
add_column :sent_notifications, :line_code, :string
diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
index 1a761fe0f86..905332b7dc7 100644
--- a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
+++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectIdToCiCommit < ActiveRecord::Migration
def up
add_column :ci_commits, :gl_project_id, :integer
diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
index 2be57b6062e..fb0e0ba1fa5 100644
--- a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
+++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateProjectIdForCiCommits < ActiveRecord::Migration
def up
subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id'
diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb
index c2ee498ef3f..71a8ae3938a 100644
--- a/db/migrate/20150930001110_merge_request_error_field.rb
+++ b/db/migrate/20150930001110_merge_request_error_field.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MergeRequestErrorField < ActiveRecord::Migration
def up
add_column :merge_requests, :merge_error, :string
diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
index 8d47dac6441..229c9942b50 100644
--- a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
+++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNullToNameForCiProjects < ActiveRecord::Migration
def up
change_column_null :ci_projects, :name, true
diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb
index 78d1a4538f2..96938bf9ab6 100644
--- a/db/migrate/20150930110012_add_group_share_lock.rb
+++ b/db/migrate/20150930110012_add_group_share_lock.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddGroupShareLock < ActiveRecord::Migration
def change
add_column :namespaces, :share_with_group_lock, :boolean, default: false
diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
index 68a745ffef4..4297ba0e7c8 100644
--- a/db/migrate/20151002112914_add_stage_idx_to_builds.rb
+++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddStageIdxToBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :stage_idx, :integer
diff --git a/db/migrate/20151002121400_add_index_for_builds.rb b/db/migrate/20151002121400_add_index_for_builds.rb
index 4ffc1363910..bd945c54540 100644
--- a/db/migrate/20151002121400_add_index_for_builds.rb
+++ b/db/migrate/20151002121400_add_index_for_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexForBuilds < ActiveRecord::Migration
def up
add_index :ci_builds, [:commit_id, :stage_idx, :created_at]
diff --git a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
index e3d2ac1cea5..3c0fcf6c45d 100644
--- a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
+++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRefAndTagToBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :tag, :boolean
diff --git a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
index 01d7b3f6773..52217ce5af2 100644
--- a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
+++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateRefAndTagToBuild < ActiveRecord::Migration
def change
execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL')
diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb
index 0f4b92b8b79..be9d403e002 100644
--- a/db/migrate/20151005075649_add_user_id_to_build.rb
+++ b/db/migrate/20151005075649_add_user_id_to_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUserIdToBuild < ActiveRecord::Migration
def change
add_column :ci_builds, :user_id, :integer
diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb
index ead9b1f8977..7e68606969f 100644
--- a/db/migrate/20151005150751_add_layout_option_for_users.rb
+++ b/db/migrate/20151005150751_add_layout_option_for_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLayoutOptionForUsers < ActiveRecord::Migration
def change
add_column :users, :layout, :integer, default: 0
diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
index be6aa810bb5..07dba598749 100644
--- a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
+++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration
def change
remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true
diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb
index 7f6cd6d5a78..38208e59804 100644
--- a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb
+++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration
disable_ddl_transaction!
diff --git a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb
index 2f2dc776785..6080d2a0fcf 100644
--- a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb
+++ b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
disable_ddl_transaction!
diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
index c72b1c611c6..a19eb6c6c49 100644
--- a/db/migrate/20151008123042_add_type_and_description_to_builds.rb
+++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :type, :string
diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
index f5c44babd84..306fa7092ea 100644
--- a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
+++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration
def change
execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL")
diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
index 0bb581efe2c..f48ec9aa4a6 100644
--- a/db/migrate/20151008143519_add_admin_notification_email_setting.rb
+++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAdminNotificationEmailSetting < ActiveRecord::Migration
def change
add_column :application_settings, :admin_notification_email, :string
diff --git a/db/migrate/20151012173029_set_jira_service_api_url.rb b/db/migrate/20151012173029_set_jira_service_api_url.rb
index 2af99e0db0b..2b6f61428c0 100644
--- a/db/migrate/20151012173029_set_jira_service_api_url.rb
+++ b/db/migrate/20151012173029_set_jira_service_api_url.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class SetJiraServiceApiUrl < ActiveRecord::Migration
# This migration can be performed online without errors, but some Jira API calls may be missed
# when doing so because api_url is not yet available.
diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
index 5a299f7b26d..a54ac9d57a4 100644
--- a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
+++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddArtifactsFileToBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_file, :text
diff --git a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb
index 52a47aa9c54..eb3351eb767 100644
--- a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb
+++ b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiProjectsGlProjectIdIndex < ActiveRecord::Migration
def change
add_index :ci_commits, :gl_project_id
diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
index 7f1af1c7583..899e004d610 100644
--- a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
+++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration
def change
add_index :ci_projects, :gitlab_id
diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb
index aeeb1a759fa..3298630c1e8 100644
--- a/db/migrate/20151016195706_add_notes_line_code_index.rb
+++ b/db/migrate/20151016195706_add_notes_line_code_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotesLineCodeIndex < ActiveRecord::Migration
def change
add_index :notes, :line_code
diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb
index 299a24b0a7c..8c05acfc190 100644
--- a/db/migrate/20151019111551_fix_build_tags.rb
+++ b/db/migrate/20151019111551_fix_build_tags.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FixBuildTags < ActiveRecord::Migration
def up
execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'")
diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb
index dcdb5d1b25d..362e31eb435 100644
--- a/db/migrate/20151019111703_fail_build_without_names.rb
+++ b/db/migrate/20151019111703_fail_build_without_names.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FailBuildWithoutNames < ActiveRecord::Migration
def up
execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'")
diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb
index 1b04f313565..14ff07bd726 100644
--- a/db/migrate/20151020145526_add_services_template_index.rb
+++ b/db/migrate/20151020145526_add_services_template_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddServicesTemplateIndex < ActiveRecord::Migration
def change
add_index :services, :template
diff --git a/db/migrate/20151020173516_ci_limits_to_mysql.rb b/db/migrate/20151020173516_ci_limits_to_mysql.rb
index 9bb960082f5..5314611cbcd 100644
--- a/db/migrate/20151020173516_ci_limits_to_mysql.rb
+++ b/db/migrate/20151020173516_ci_limits_to_mysql.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CiLimitsToMysql < ActiveRecord::Migration
def change
return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
diff --git a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb
index c3f0e0606da..81a31e46ff8 100644
--- a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb
+++ b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiBuildsIndexForStatus < ActiveRecord::Migration
def change
add_index :ci_builds, [:commit_id, :status, :type]
diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb
index 41c0f0649cd..0666dfeaef4 100644
--- a/db/migrate/20151023112551_fail_build_with_empty_name.rb
+++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FailBuildWithEmptyName < ActiveRecord::Migration
def up
execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'")
diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb
index e73f300028a..98fe0bd7d1d 100644
--- a/db/migrate/20151023144219_remove_satellites.rb
+++ b/db/migrate/20151023144219_remove_satellites.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
require 'fileutils'
class RemoveSatellites < ActiveRecord::Migration
diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb
index a62fe199d70..117f65c1a1b 100644
--- a/db/migrate/20151026182941_add_project_path_index.rb
+++ b/db/migrate/20151026182941_add_project_path_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectPathIndex < ActiveRecord::Migration
def up
add_index :projects, :path
diff --git a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb
index ceb52f0c222..4a989669464 100644
--- a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb
+++ b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :merge_params, :text
diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb
index 635346300c2..ba1f7c27832 100644
--- a/db/migrate/20151103001141_add_public_to_group.rb
+++ b/db/migrate/20151103001141_add_public_to_group.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPublicToGroup < ActiveRecord::Migration
def change
add_column :namespaces, :public, :boolean, default: false
diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb
index 4231dfd5c2e..b5b34d4ca61 100644
--- a/db/migrate/20151103133339_add_shared_runners_setting.rb
+++ b/db/migrate/20151103133339_add_shared_runners_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSharedRunnersSetting < ActiveRecord::Migration
def up
add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false
diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb
index 2d04c170a88..745b52e2b24 100644
--- a/db/migrate/20151103134857_create_lfs_objects.rb
+++ b/db/migrate/20151103134857_create_lfs_objects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateLfsObjects < ActiveRecord::Migration
def change
create_table :lfs_objects do |t|
diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb
index f3f58b931ec..3178e85b899 100644
--- a/db/migrate/20151103134958_create_lfs_objects_projects.rb
+++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateLfsObjectsProjects < ActiveRecord::Migration
def change
create_table :lfs_objects_projects do |t|
diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
index 7c57f3f0df6..4e46ae8101c 100644
--- a/db/migrate/20151104105513_add_file_to_lfs_objects.rb
+++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddFileToLfsObjects < ActiveRecord::Migration
def change
add_column :lfs_objects, :file, :string
diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb
index fe4608c6662..145b8db1486 100644
--- a/db/migrate/20151105094515_create_releases.rb
+++ b/db/migrate/20151105094515_create_releases.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateReleases < ActiveRecord::Migration
def change
create_table :releases do |t|
diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb
index 02b271637e9..b463d939b78 100644
--- a/db/migrate/20151106000015_add_is_award_to_notes.rb
+++ b/db/migrate/20151106000015_add_is_award_to_notes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIsAwardToNotes < ActiveRecord::Migration
def change
add_column :notes, :is_award, :boolean, default: false, null: false
diff --git a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
index 01d8c0f043e..25106ace7e9 100644
--- a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
+++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false
diff --git a/db/migrate/20151109134526_add_issues_state_index.rb b/db/migrate/20151109134526_add_issues_state_index.rb
index 1c4d2e30171..7a9970e8591 100644
--- a/db/migrate/20151109134526_add_issues_state_index.rb
+++ b/db/migrate/20151109134526_add_issues_state_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIssuesStateIndex < ActiveRecord::Migration
def change
add_index :issues, :state
diff --git a/db/migrate/20151109134916_add_projects_visibility_level_index.rb b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
index 600b4bafd98..471db437b11 100644
--- a/db/migrate/20151109134916_add_projects_visibility_level_index.rb
+++ b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectsVisibilityLevelIndex < ActiveRecord::Migration
def change
add_index :projects, :visibility_level
diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb
index 7fc990f8d0a..793358c305e 100644
--- a/db/migrate/20151110125604_add_import_error_to_project.rb
+++ b/db/migrate/20151110125604_add_import_error_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportErrorToProject < ActiveRecord::Migration
def change
add_column :projects, :import_error, :text
diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
index d10f1f6e605..00a4c74ffbc 100644
--- a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
+++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexForLfsOidAndSize < ActiveRecord::Migration
def change
add_index :lfs_objects, :oid
diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
index 41b93da0a86..1f192544ea1 100644
--- a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
+++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUniqueForLfsOidIndex < ActiveRecord::Migration
def change
remove_index :lfs_objects, :oid
diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb
index fded70e3c0c..589f124c21e 100644
--- a/db/migrate/20151118162244_add_projects_public_index.rb
+++ b/db/migrate/20151118162244_add_projects_public_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectsPublicIndex < ActiveRecord::Migration
def change
add_index :namespaces, :public
diff --git a/db/migrate/20151201203948_raise_hook_url_limit.rb b/db/migrate/20151201203948_raise_hook_url_limit.rb
index 98a7fca6f6f..c490b7ace0f 100644
--- a/db/migrate/20151201203948_raise_hook_url_limit.rb
+++ b/db/migrate/20151201203948_raise_hook_url_limit.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RaiseHookUrlLimit < ActiveRecord::Migration
def change
change_column :web_hooks, :url, :string, limit: 2000
diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
index 6ffadfa1894..5dc6d8bf445 100644
--- a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
+++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddHideProjectLimitToUsers < ActiveRecord::Migration
def change
add_column :users, :hide_project_limit, :boolean, default: false
diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb
index c5542cb864d..455882e5ec0 100644
--- a/db/migrate/20151203162134_add_build_events_to_services.rb
+++ b/db/migrate/20151203162134_add_build_events_to_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddBuildEventsToServices < ActiveRecord::Migration
def change
add_column :services, :build_events, :boolean, default: false, null: false
diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
index d7e196e6763..cb1e556623a 100644
--- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb
+++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateCiWebHooks < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb
index 7f330a2cf0a..6b7a106814d 100644
--- a/db/migrate/20151209145909_migrate_ci_emails.rb
+++ b/db/migrate/20151209145909_migrate_ci_emails.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateCiEmails < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb
index 0ea66ba65df..d23c648f782 100644
--- a/db/migrate/20151210030143_add_unlock_token_to_user.rb
+++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddUnlockTokenToUser < ActiveRecord::Migration
def change
add_column :users, :unlock_token, :string
diff --git a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb
index 00f88180e46..92c7b5befd2 100644
--- a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb
+++ b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRunnersRegistrationTokenToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :runners_registration_token, :string
diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb
index f14efa3e95d..633d5148d97 100644
--- a/db/migrate/20151210125232_migrate_ci_slack_service.rb
+++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateCiSlackService < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
index b9e04323576..dae084ce180 100644
--- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
+++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateCiHipChatService < ActiveRecord::Migration
include Gitlab::Database
diff --git a/db/migrate/20151210125928_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb
index 8c167f64a2b..a9ff49a3f7e 100644
--- a/db/migrate/20151210125928_add_ci_to_project.rb
+++ b/db/migrate/20151210125928_add_ci_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiToProject < ActiveRecord::Migration
def change
add_column :projects, :ci_id, :integer
diff --git a/db/migrate/20151210125929_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb
index 84273591fa2..b5de64b82ca 100644
--- a/db/migrate/20151210125929_add_project_id_to_ci.rb
+++ b/db/migrate/20151210125929_add_project_id_to_ci.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddProjectIdToCi < ActiveRecord::Migration
def change
add_column :ci_builds, :gl_project_id, :integer
diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb
index c32c7feb193..bb6d74ae212 100644
--- a/db/migrate/20151210125930_migrate_ci_to_project.rb
+++ b/db/migrate/20151210125930_migrate_ci_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class MigrateCiToProject < ActiveRecord::Migration
def up
migrate_project_id_for_table('ci_runner_projects')
diff --git a/db/migrate/20151210125931_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb
index 5e129c9303d..d87d335cf6b 100644
--- a/db/migrate/20151210125931_add_index_to_ci_tables.rb
+++ b/db/migrate/20151210125931_add_index_to_ci_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexToCiTables < ActiveRecord::Migration
def change
add_index :ci_builds, :gl_project_id
diff --git a/db/migrate/20151210125932_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb
index c520c2ed56f..e1a0a964589 100644
--- a/db/migrate/20151210125932_drop_null_for_ci_tables.rb
+++ b/db/migrate/20151210125932_drop_null_for_ci_tables.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class DropNullForCiTables < ActiveRecord::Migration
def change
remove_index :ci_variables, :project_id
diff --git a/db/migrate/20151218154042_add_tfa_to_application_settings.rb b/db/migrate/20151218154042_add_tfa_to_application_settings.rb
index dd95db775c5..afdaf76b917 100644
--- a/db/migrate/20151218154042_add_tfa_to_application_settings.rb
+++ b/db/migrate/20151218154042_add_tfa_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTfaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
diff --git a/db/migrate/20151221234414_add_tfa_additional_fields.rb b/db/migrate/20151221234414_add_tfa_additional_fields.rb
index c16df47932f..c3e4aaa606a 100644
--- a/db/migrate/20151221234414_add_tfa_additional_fields.rb
+++ b/db/migrate/20151221234414_add_tfa_additional_fields.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTfaAdditionalFields < ActiveRecord::Migration
def change
change_table :users do |t|
diff --git a/db/migrate/20151224123230_rename_emojis.rb b/db/migrate/20151224123230_rename_emojis.rb
index 62d921dfdcc..2c24f3beeea 100644
--- a/db/migrate/20151224123230_rename_emojis.rb
+++ b/db/migrate/20151224123230_rename_emojis.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# Migration type: online without errors (works on previous version and new one)
class RenameEmojis < ActiveRecord::Migration
def up
diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb
index f4c848bbf47..bcb322d9cba 100644
--- a/db/migrate/20151228111122_remove_public_from_namespace.rb
+++ b/db/migrate/20151228111122_remove_public_from_namespace.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# Migration type: online
class RemovePublicFromNamespace < ActiveRecord::Migration
def change
diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb
index 3012bd52cfd..2e080a02e6a 100644
--- a/db/migrate/20151228150906_influxdb_settings.rb
+++ b/db/migrate/20151228150906_influxdb_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class InfluxdbSettings < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_enabled, :boolean, default: false
diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
index 259fd0248d2..e0dd19b2b06 100644
--- a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
+++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRecaptchaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
index ae0499f936d..3e1bfd43899 100644
--- a/db/migrate/20151229102248_influxdb_udp_port_setting.rb
+++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class InfluxdbUdpPortSetting < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_port, :integer, default: 8089
diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
index f0e1ee1e7a7..d2ac906ead3 100644
--- a/db/migrate/20151229112614_influxdb_remote_database_setting.rb
+++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration
def change
remove_column :application_settings, :metrics_database
diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
index 6c282fc5039..4fcca06d905 100644
--- a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
+++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_metadata, :text
diff --git a/db/migrate/20151231152326_add_akismet_to_application_settings.rb b/db/migrate/20151231152326_add_akismet_to_application_settings.rb
index 3f52c758f9a..7b0fab6f557 100644
--- a/db/migrate/20151231152326_add_akismet_to_application_settings.rb
+++ b/db/migrate/20151231152326_add_akismet_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAkismetToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
index 78fdfeaf5cf..0bdd639eb21 100644
--- a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
+++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration
def change
remove_column :broadcast_messages, :alert_type, :integer
diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb
index 767885e2aac..9b9b6445a08 100644
--- a/db/migrate/20160106162223_add_index_milestones_title.rb
+++ b/db/migrate/20160106162223_add_index_milestones_title.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexMilestonesTitle < ActiveRecord::Migration
def change
add_index :milestones, :title
diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb
index 47e74400b97..987d75d6fda 100644
--- a/db/migrate/20160106164438_remove_influxdb_credentials.rb
+++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveInfluxdbCredentials < ActiveRecord::Migration
def change
remove_column :application_settings, :metrics_username, :string
diff --git a/db/migrate/20160109054846_create_spam_logs.rb b/db/migrate/20160109054846_create_spam_logs.rb
index f12fe9f8f78..f7103276639 100644
--- a/db/migrate/20160109054846_create_spam_logs.rb
+++ b/db/migrate/20160109054846_create_spam_logs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateSpamLogs < ActiveRecord::Migration
def change
create_table :spam_logs do |t|
diff --git a/db/migrate/20160113111034_add_metrics_sample_interval.rb b/db/migrate/20160113111034_add_metrics_sample_interval.rb
index b741f5d2c75..c1041da818c 100644
--- a/db/migrate/20160113111034_add_metrics_sample_interval.rb
+++ b/db/migrate/20160113111034_add_metrics_sample_interval.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMetricsSampleInterval < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_sample_interval, :integer,
diff --git a/db/migrate/20160118155830_add_sentry_to_application_settings.rb b/db/migrate/20160118155830_add_sentry_to_application_settings.rb
index fa7ff9d9228..a6f715263ef 100644
--- a/db/migrate/20160118155830_add_sentry_to_application_settings.rb
+++ b/db/migrate/20160118155830_add_sentry_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSentryToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
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
index 26606b10b54..19ea40b5547 100644
--- a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
+++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :ip_blocking_enabled, :boolean, default: false
diff --git a/db/migrate/20160119111158_add_services_category.rb b/db/migrate/20160119111158_add_services_category.rb
index a9110a8418b..f77484b2f96 100644
--- a/db/migrate/20160119111158_add_services_category.rb
+++ b/db/migrate/20160119111158_add_services_category.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddServicesCategory < ActiveRecord::Migration
def up
add_column :services, :category, :string, default: 'common', null: false
diff --git a/db/migrate/20160119112418_add_services_default.rb b/db/migrate/20160119112418_add_services_default.rb
index 69a42d7b873..7fa531899fe 100644
--- a/db/migrate/20160119112418_add_services_default.rb
+++ b/db/migrate/20160119112418_add_services_default.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddServicesDefault < ActiveRecord::Migration
def up
add_column :services, :default, :boolean, default: false
diff --git a/db/migrate/20160119145451_add_ldap_email_to_users.rb b/db/migrate/20160119145451_add_ldap_email_to_users.rb
index 654d31ab15a..5b2b0bd31ca 100644
--- a/db/migrate/20160119145451_add_ldap_email_to_users.rb
+++ b/db/migrate/20160119145451_add_ldap_email_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddLdapEmailToUsers < ActiveRecord::Migration
def up
add_column :users, :ldap_email, :boolean, default: false, null: false
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
index d6c6aa4a4e8..3837208f81e 100644
--- 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
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddBaseCommitShaToMergeRequestDiffs < ActiveRecord::Migration
def change
add_column :merge_request_diffs, :base_commit_sha, :string
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
index d50791410f9..9a2570ae544 100644
--- 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
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEmailAuthorInBodyToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :email_author_in_body, :boolean, default: false
diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb
index 046a5d8fc32..61db852843f 100644
--- a/db/migrate/20160122185421_add_pending_delete_to_project.rb
+++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddPendingDeleteToProject < ActiveRecord::Migration
def change
add_column :projects, :pending_delete, :boolean, default: false
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
index 41821cdcc42..60ecda998dd 100644
--- a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb
+++ b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveIpBlockingSettingsFromApplicationSettings < ActiveRecord::Migration
def change
remove_column :application_settings, :ip_blocking_enabled, :boolean, default: false
diff --git a/db/migrate/20160128233227_change_lfs_objects_size_column.rb b/db/migrate/20160128233227_change_lfs_objects_size_column.rb
index e7fd1f71777..645c0cdb192 100644
--- a/db/migrate/20160128233227_change_lfs_objects_size_column.rb
+++ b/db/migrate/20160128233227_change_lfs_objects_size_column.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ChangeLfsObjectsSizeColumn < ActiveRecord::Migration
def change
change_column :lfs_objects, :size, :integer, limit: 8
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
index d3ea956952e..b10c0602e24 100644
--- a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb
+++ b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveDotAtomPathEndingOfProjects < ActiveRecord::Migration
include Gitlab::ShellAdapter
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
index f0d94226514..332b5a756e8 100644
--- a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb
+++ b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMergeCommitShaToMergeRequests < ActiveRecord::Migration
def change
add_column :merge_requests, :merge_commit_sha, :string
diff --git a/db/migrate/20160202091601_add_erasable_to_ci_build.rb b/db/migrate/20160202091601_add_erasable_to_ci_build.rb
index f9912f2274e..767ae160d08 100644
--- a/db/migrate/20160202091601_add_erasable_to_ci_build.rb
+++ b/db/migrate/20160202091601_add_erasable_to_ci_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddErasableToCiBuild < ActiveRecord::Migration
def change
add_reference :ci_builds, :erased_by, references: :users, index: true
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
index 793984343b4..2c5cb307fad 100644
--- a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb
+++ b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddAllowGuestToAccessBuildsProject < ActiveRecord::Migration
def change
add_column :projects, :public_builds, :boolean, default: true, null: false
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
index f996ae74dca..11b6ff31000 100644
--- a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb
+++ b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRealSizeToMergeRequestDiffs < ActiveRecord::Migration
def change
add_column :merge_request_diffs, :real_size, :string
diff --git a/db/migrate/20160209130428_add_index_to_snippet.rb b/db/migrate/20160209130428_add_index_to_snippet.rb
index 95d5719be59..4d17c3a2917 100644
--- a/db/migrate/20160209130428_add_index_to_snippet.rb
+++ b/db/migrate/20160209130428_add_index_to_snippet.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexToSnippet < ActiveRecord::Migration
def change
add_index :snippets, :updated_at
diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb
index c3f6f3abc26..20573b01351 100644
--- a/db/migrate/20160212123307_create_tasks.rb
+++ b/db/migrate/20160212123307_create_tasks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateTasks < ActiveRecord::Migration
def change
create_table :tasks do |t|
diff --git a/db/migrate/20160217100506_add_description_to_label.rb b/db/migrate/20160217100506_add_description_to_label.rb
index eed6d1f236a..af5af167470 100644
--- a/db/migrate/20160217100506_add_description_to_label.rb
+++ b/db/migrate/20160217100506_add_description_to_label.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDescriptionToLabel < ActiveRecord::Migration
def change
add_column :labels, :description, :string
diff --git a/db/migrate/20160217174422_add_note_to_tasks.rb b/db/migrate/20160217174422_add_note_to_tasks.rb
index da5cb2e05db..a9a2b77e423 100644
--- a/db/migrate/20160217174422_add_note_to_tasks.rb
+++ b/db/migrate/20160217174422_add_note_to_tasks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNoteToTasks < ActiveRecord::Migration
def change
add_reference :tasks, :note, index: true
diff --git a/db/migrate/20160220123949_rename_tasks_to_todos.rb b/db/migrate/20160220123949_rename_tasks_to_todos.rb
index 30c10d27146..f16b37537f3 100644
--- a/db/migrate/20160220123949_rename_tasks_to_todos.rb
+++ b/db/migrate/20160220123949_rename_tasks_to_todos.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RenameTasksToTodos < ActiveRecord::Migration
def change
rename_table :tasks, :todos
diff --git a/db/migrate/20160222153918_create_appearances_ce.rb b/db/migrate/20160222153918_create_appearances_ce.rb
index bec66bcc71e..b2d5949b23f 100644
--- a/db/migrate/20160222153918_create_appearances_ce.rb
+++ b/db/migrate/20160222153918_create_appearances_ce.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateAppearancesCe < ActiveRecord::Migration
def change
unless table_exists?(:appearances)
diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb
index e9d47fd589a..5b99ce30e9f 100644
--- a/db/migrate/20160223192159_add_confidential_to_issues.rb
+++ b/db/migrate/20160223192159_add_confidential_to_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddConfidentialToIssues < ActiveRecord::Migration
def change
add_column :issues, :confidential, :boolean, default: false
diff --git a/db/migrate/20160225090018_add_delete_at_to_issues.rb b/db/migrate/20160225090018_add_delete_at_to_issues.rb
index 3ddbef92978..139f911e1c9 100644
--- a/db/migrate/20160225090018_add_delete_at_to_issues.rb
+++ b/db/migrate/20160225090018_add_delete_at_to_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDeleteAtToIssues < ActiveRecord::Migration
def change
add_column :issues, :deleted_at, :datetime
diff --git a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb
index 9d09105f17d..4ca3f0dcdc5 100644
--- a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb
+++ b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDeleteAtToMergeRequests < ActiveRecord::Migration
def change
add_column :merge_requests, :deleted_at, :datetime
diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
index d7b00e3d6ed..375e389e07a 100644
--- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTrigramIndexesForSearching < ActiveRecord::Migration
disable_ddl_transaction!
diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
index 65f2a47bb3c..89910893ee1 100644
--- a/db/migrate/20160227120001_add_event_field_for_web_hook.rb
+++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEventFieldForWebHook < ActiveRecord::Migration
def change
add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false
diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb
index f5040d770de..fe7c54ca4eb 100644
--- a/db/migrate/20160227120047_add_event_to_services.rb
+++ b/db/migrate/20160227120047_add_event_to_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddEventToServices < ActiveRecord::Migration
def change
add_column :services, :wiki_page_events, :boolean, default: true
diff --git a/db/migrate/20160229193553_add_main_language_to_repository.rb b/db/migrate/20160229193553_add_main_language_to_repository.rb
index b5446c6a447..ad5167b4c93 100644
--- a/db/migrate/20160229193553_add_main_language_to_repository.rb
+++ b/db/migrate/20160229193553_add_main_language_to_repository.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMainLanguageToRepository < ActiveRecord::Migration
def change
add_column :projects, :main_language, :string
diff --git a/db/migrate/20160301124843_add_visibility_level_to_groups.rb b/db/migrate/20160301124843_add_visibility_level_to_groups.rb
index d1b921bb208..a874e6758dd 100644
--- a/db/migrate/20160301124843_add_visibility_level_to_groups.rb
+++ b/db/migrate/20160301124843_add_visibility_level_to_groups.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddVisibilityLevelToGroups < ActiveRecord::Migration
def up
add_column :namespaces, :visibility_level, :integer, null: false, default: Gitlab::VisibilityLevel::PUBLIC
diff --git a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb
index ffcd64266e3..1f400566f9f 100644
--- a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb
+++ b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddImportCredentialsToProjectImportData < ActiveRecord::Migration
def change
add_column :project_import_data, :encrypted_credentials, :text
diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
index 8a351cf27a3..ac7eac0ea7c 100644
--- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
+++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# Loops through old importer projects that kept a token/password in the import URL
# and encrypts the credentials into a separate field in project#import_data
# #down method not supported
@@ -24,11 +25,11 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration
def process_projects_with_wrong_url
projects_with_wrong_import_url.each do |project|
begin
- import_url = Gitlab::ImportUrl.new(project["import_url"])
+ import_url = Gitlab::UrlSanitizer.new(project["import_url"])
update_import_url(import_url, project)
update_import_data(import_url, project)
- rescue URI::InvalidURIError
+ rescue Addressable::URI::InvalidURIError
nullify_import_url(project)
end
end
diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb
index fc12b5b09e6..cac78703bc2 100644
--- a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb
+++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveExpiresAtFromSnippets < ActiveRecord::Migration
def change
remove_column :snippets, :expires_at, :datetime
diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
index 49e787d9a9a..10f2b8cc56a 100644
--- a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
+++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
def up
execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb
index 75de5f70fa2..92c0a1e088e 100644
--- a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb
+++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# Create visibility level field on DB
# Sets default_visibility_level to value on settings if not restricted
# If value is restricted takes higher visibility level allowed
@@ -7,7 +8,9 @@ class AddDefaultGroupVisibilityToApplicationSettings < ActiveRecord::Migration
add_column :application_settings, :default_group_visibility, :integer
# Unfortunately, this can't be a `default`, since we don't want the configuration specific
# `allowed_visibility_level` to end up in schema.rb
- execute("UPDATE application_settings SET default_group_visibility = #{allowed_visibility_level}")
+
+ visibility_level = allowed_visibility_level || Gitlab::VisibilityLevel::PRIVATE
+ execute("UPDATE application_settings SET default_group_visibility = #{visibility_level}")
end
def down
diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb
index ebe0fc82305..94fe1e4fdc3 100644
--- a/db/migrate/20160309140734_fix_todos.rb
+++ b/db/migrate/20160309140734_fix_todos.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class FixTodos < ActiveRecord::Migration
def up
execute <<-SQL
diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb
index ec08bd9fdfa..a4eb6aaee63 100644
--- a/db/migrate/20160310124959_add_due_date_to_issues.rb
+++ b/db/migrate/20160310124959_add_due_date_to_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddDueDateToIssues < ActiveRecord::Migration
def change
add_column :issues, :due_date, :date
diff --git a/db/migrate/20160310185910_add_external_flag_to_users.rb b/db/migrate/20160310185910_add_external_flag_to_users.rb
index 54937f1eb71..209496dc786 100644
--- a/db/migrate/20160310185910_add_external_flag_to_users.rb
+++ b/db/migrate/20160310185910_add_external_flag_to_users.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddExternalFlagToUsers < ActiveRecord::Migration
def change
add_column :users, :external, :boolean, default: false
diff --git a/db/migrate/20160314094147_add_priority_to_label.rb b/db/migrate/20160314094147_add_priority_to_label.rb
new file mode 100644
index 00000000000..7fb23cba4c9
--- /dev/null
+++ b/db/migrate/20160314094147_add_priority_to_label.rb
@@ -0,0 +1,7 @@
+# rubocop:disable all
+class AddPriorityToLabel < ActiveRecord::Migration
+ def change
+ add_column :labels, :priority, :integer
+ add_index :labels, :priority
+ end
+end
diff --git a/db/migrate/20160314114439_add_requested_at_to_members.rb b/db/migrate/20160314114439_add_requested_at_to_members.rb
new file mode 100644
index 00000000000..273819d4cd8
--- /dev/null
+++ b/db/migrate/20160314114439_add_requested_at_to_members.rb
@@ -0,0 +1,5 @@
+class AddRequestedAtToMembers < ActiveRecord::Migration
+ def change
+ add_column :members, :requested_at, :datetime
+ end
+end
diff --git a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
index 5d30a38bc99..9f8ffe073a3 100644
--- a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
+++ b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ProjectsAddPushesSinceGc < ActiveRecord::Migration
def change
add_column :projects, :pushes_since_gc, :integer, default: 0
diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb
index 8687d5d6296..8fe649246c7 100644
--- a/db/migrate/20160315135439_project_add_repository_check.rb
+++ b/db/migrate/20160315135439_project_add_repository_check.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ProjectAddRepositoryCheck < ActiveRecord::Migration
def change
add_column :projects, :last_repository_check_failed, :boolean
diff --git a/db/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb
index 67bf5b4f978..ff3d36d68ee 100644
--- a/db/migrate/20160316123110_ci_runners_token_index.rb
+++ b/db/migrate/20160316123110_ci_runners_token_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CiRunnersTokenIndex < ActiveRecord::Migration
disable_ddl_transaction!
diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
index 6871b3920df..65e0e61c78f 100644
--- a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
+++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration
def change
change_column_null :todos, :target_id, true
diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb
index ae19fdd1abd..d79858fc920 100644
--- a/db/migrate/20160316204731_add_commit_id_to_todos.rb
+++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCommitIdToTodos < ActiveRecord::Migration
def change
add_column :todos, :commit_id, :string
diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb
index 461e7fb3a9b..9dde668ddff 100644
--- a/db/migrate/20160317092222_add_moved_to_to_issue.rb
+++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMovedToToIssue < ActiveRecord::Migration
def change
add_reference :issues, :moved_to, references: :issues
diff --git a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb
index 370b339d45c..07ae7c95477 100644
--- a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb
+++ b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class IndexNamespacesOnVisibilityLevel < ActiveRecord::Migration
def change
unless index_exists?(:namespaces, :visibility_level)
diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
index 1fff9759d1e..a9a851cfe63 100644
--- a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
+++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveTodosForDeletedIssues < ActiveRecord::Migration
def up
execute <<-SQL
diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb
index 4755da8b806..7d77e8004ba 100644
--- a/db/migrate/20160328112808_create_notification_settings.rb
+++ b/db/migrate/20160328112808_create_notification_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class CreateNotificationSettings < ActiveRecord::Migration
def change
create_table :notification_settings do |t|
diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb
index 3c81b2c37bf..eb6b7d07219 100644
--- a/db/migrate/20160328115649_migrate_new_notification_setting.rb
+++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
# This migration will create one row of NotificationSetting for each Member row
# It can take long time on big instances.
#
diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb
index 8aebce0244d..667270d6b04 100644
--- a/db/migrate/20160328121138_add_notification_setting_index.rb
+++ b/db/migrate/20160328121138_add_notification_setting_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddNotificationSettingIndex < ActiveRecord::Migration
def change
add_index :notification_settings, :user_id
diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
index 275554e736e..a3df8fb4e2e 100644
--- a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
+++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration
def change
add_index :projects, :pending_delete
diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
index 54cea964ff2..b15af79b9b5 100644
--- a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
+++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration
def up
execute <<-SQL
diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
index 0d736e323b6..dec80497fb3 100644
--- a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
+++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration
def change
remove_column :application_settings, :twitter_sharing_enabled, :boolean
diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb
new file mode 100644
index 00000000000..fcffc98b47a
--- /dev/null
+++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class AddImagesEnabledForProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :container_registry_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb
index ebfa4bcbc7b..920d4d41110 100644
--- a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb
+++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration
def change
add_column :application_settings, :repository_checks_enabled, :boolean, default: true
diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb
index 125956a3ddd..00162af5cda 100644
--- a/db/migrate/20160412173416_add_fields_to_ci_commit.rb
+++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddFieldsToCiCommit < ActiveRecord::Migration
def change
add_column :ci_commits, :status, :string
diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb
index fd92444dbac..858faeb060e 100644
--- a/db/migrate/20160412173417_update_ci_commit.rb
+++ b/db/migrate/20160412173417_update_ci_commit.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class UpdateCiCommit < ActiveRecord::Migration
# This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers
# Otherwise Offline migration should be used.
diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb
index 603d4a41610..414f1f8279f 100644
--- a/db/migrate/20160412173418_add_ci_commit_indexes.rb
+++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddCiCommitIndexes < ActiveRecord::Migration
disable_ddl_transaction!
diff --git a/db/migrate/20160413115152_add_token_to_web_hooks.rb b/db/migrate/20160413115152_add_token_to_web_hooks.rb
index f04225068cd..628b1d51b30 100644
--- a/db/migrate/20160413115152_add_token_to_web_hooks.rb
+++ b/db/migrate/20160413115152_add_token_to_web_hooks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddTokenToWebHooks < ActiveRecord::Migration
def change
add_column :web_hooks, :token, :string
diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb
new file mode 100644
index 00000000000..ce0b33f32bd
--- /dev/null
+++ b/db/migrate/20160415062917_create_personal_access_tokens.rb
@@ -0,0 +1,13 @@
+class CreatePersonalAccessTokens < ActiveRecord::Migration
+ def change
+ create_table :personal_access_tokens do |t|
+ t.references :user, index: true, foreign_key: true, null: false
+ t.string :token, index: { unique: true }, null: false
+ t.string :name, null: false
+ t.boolean :revoked, default: false
+ t.datetime :expires_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
index d493044c67b..b53b9bc6c3d 100644
--- a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
+++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :shared_runners_text, :text
diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb
new file mode 100644
index 00000000000..a3bee9b1bc6
--- /dev/null
+++ b/db/migrate/20160416180807_add_award_emoji.rb
@@ -0,0 +1,15 @@
+# rubocop:disable all
+class AddAwardEmoji < ActiveRecord::Migration
+ def change
+ create_table :award_emoji do |t|
+ t.string :name
+ t.references :user
+ t.references :awardable, polymorphic: true
+
+ t.timestamps
+ end
+
+ add_index :award_emoji, :user_id
+ add_index :award_emoji, [:awardable_type, :awardable_id]
+ end
+end
diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
new file mode 100644
index 00000000000..95ee03611d9
--- /dev/null
+++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
@@ -0,0 +1,37 @@
+# rubocop:disable all
+class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ migrate_postgresql
+ else
+ migrate_mysql
+ end
+ end
+
+ def down
+ add_column :notes, :is_award, :boolean
+
+ # This migration does NOT move the awards on notes, if the table is dropped in another migration, these notes will be lost.
+ execute "INSERT INTO notes (noteable_type, noteable_id, author_id, note, created_at, updated_at, is_award) (SELECT awardable_type, awardable_id, user_id, name, created_at, updated_at, TRUE FROM award_emoji)"
+ end
+
+ def migrate_postgresql
+ connection.transaction do
+ execute 'LOCK notes IN EXCLUSIVE MODE'
+ execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
+ execute "DELETE FROM notes WHERE is_award = true"
+ remove_column :notes, :is_award, :boolean
+ end
+ end
+
+ def migrate_mysql
+ execute 'LOCK TABLES notes WRITE, award_emoji WRITE;'
+ execute 'INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true);'
+ execute "DELETE FROM notes WHERE is_award = true"
+ remove_column :notes, :is_award, :boolean
+ ensure
+ execute 'UNLOCK TABLES'
+ end
+end
diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb
index 78c163d62ac..c759427c590 100644
--- a/db/migrate/20160419120017_add_metrics_packet_size.rb
+++ b/db/migrate/20160419120017_add_metrics_packet_size.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class AddMetricsPacketSize < ActiveRecord::Migration
def change
add_column :application_settings, :metrics_packet_size, :integer, default: 1
diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
new file mode 100644
index 00000000000..69d64ccd006
--- /dev/null
+++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
@@ -0,0 +1,15 @@
+class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects,
+ :only_allow_merge_if_build_succeeds,
+ :boolean,
+ default: false)
+ end
+
+ def down
+ remove_column(:projects, :only_allow_merge_if_build_succeeds)
+ end
+end
diff --git a/db/migrate/20160421130527_disable_repository_checks.rb b/db/migrate/20160421130527_disable_repository_checks.rb
index 808a4b93c7c..7e65ddc45e7 100644
--- a/db/migrate/20160421130527_disable_repository_checks.rb
+++ b/db/migrate/20160421130527_disable_repository_checks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class DisableRepositoryChecks < ActiveRecord::Migration
def up
change_column_default :application_settings, :repository_checks_enabled, false
diff --git a/db/migrate/20160425045124_create_u2f_registrations.rb b/db/migrate/20160425045124_create_u2f_registrations.rb
new file mode 100644
index 00000000000..72cbe98ebba
--- /dev/null
+++ b/db/migrate/20160425045124_create_u2f_registrations.rb
@@ -0,0 +1,14 @@
+# rubocop:disable all
+class CreateU2fRegistrations < ActiveRecord::Migration
+ def change
+ create_table :u2f_registrations do |t|
+ t.text :certificate
+ t.string :key_handle, index: true
+ t.string :public_key
+ t.integer :counter
+ t.references :user, index: true, foreign_key: true
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb
new file mode 100644
index 00000000000..bf50616656c
--- /dev/null
+++ b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :disabled_oauth_sign_in_sources, :text
+ end
+end
diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb
new file mode 100644
index 00000000000..c60892a6279
--- /dev/null
+++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb
@@ -0,0 +1,14 @@
+# rubocop:disable all
+class AddRunUntaggedToCiRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_runners, :run_untagged, :boolean,
+ default: true, allow_null: false)
+ end
+
+ def down
+ remove_column(:ci_runners, :run_untagged)
+ end
+end
diff --git a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb
index aa560bc0f0c..6792ffc957a 100644
--- a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb
+++ b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class RemoveWallEnabledFromProjects < ActiveRecord::Migration
def change
remove_column :projects, :wall_enabled, :boolean, default: true, null: false
diff --git a/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb
new file mode 100644
index 00000000000..1c4d60e7234
--- /dev/null
+++ b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb
@@ -0,0 +1,5 @@
+class AddHeadCommitIdToMergeRequestDiffs < ActiveRecord::Migration
+ def change
+ add_column :merge_request_diffs, :head_commit_sha, :string
+ end
+end
diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb
new file mode 100644
index 00000000000..c1d07c9363f
--- /dev/null
+++ b/db/migrate/20160508215820_add_type_to_notes.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class AddTypeToNotes < ActiveRecord::Migration
+ def change
+ add_column :notes, :type, :string
+ end
+end
diff --git a/db/migrate/20160508215920_add_positions_to_diff_notes.rb b/db/migrate/20160508215920_add_positions_to_diff_notes.rb
new file mode 100644
index 00000000000..2952c25004e
--- /dev/null
+++ b/db/migrate/20160508215920_add_positions_to_diff_notes.rb
@@ -0,0 +1,6 @@
+class AddPositionsToDiffNotes < ActiveRecord::Migration
+ def change
+ add_column :notes, :position, :text
+ add_column :notes, :original_position, :text
+ end
+end
diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb
new file mode 100644
index 00000000000..6dd958ff4a0
--- /dev/null
+++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration
+ def change
+ execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL"
+ end
+end
diff --git a/db/migrate/20160509091049_add_locked_to_ci_runner.rb b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
new file mode 100644
index 00000000000..3fbaef3b7f0
--- /dev/null
+++ b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
@@ -0,0 +1,13 @@
+class AddLockedToCiRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_runners, :locked, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:ci_runners, :locked)
+ end
+end
diff --git a/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb
new file mode 100644
index 00000000000..b6a5bea79b6
--- /dev/null
+++ b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :health_check_access_token, :string
+ end
+end
diff --git a/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb
new file mode 100644
index 00000000000..8c96353b850
--- /dev/null
+++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb
@@ -0,0 +1,13 @@
+# rubocop:disable all
+class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration
+ def up
+ add_column :application_settings, :send_user_confirmation_email, :boolean, default: false
+
+ #Sets confirmation email to true by default on existing installations.
+ execute "UPDATE application_settings SET send_user_confirmation_email=true"
+ end
+
+ def down
+ remove_column :application_settings, :send_user_confirmation_email
+ end
+end
diff --git a/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb
new file mode 100644
index 00000000000..b7fd76ee84b
--- /dev/null
+++ b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb
@@ -0,0 +1,5 @@
+class AddStartCommitIdToMergeRequestDiffs < ActiveRecord::Migration
+ def change
+ add_column :merge_request_diffs, :start_commit_sha, :string
+ end
+end
diff --git a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
new file mode 100644
index 00000000000..915167b038d
--- /dev/null
+++ b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
@@ -0,0 +1,5 @@
+class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_expire_at, :timestamp
+ end
+end
diff --git a/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb
new file mode 100644
index 00000000000..4eef16c9408
--- /dev/null
+++ b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNoteTypeAndPositionToSentNotification < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :sent_notifications, :note_type, :string
+ add_column :sent_notifications, :position, :text
+ end
+end
diff --git a/db/migrate/20160525205328_remove_main_language_from_projects.rb b/db/migrate/20160525205328_remove_main_language_from_projects.rb
new file mode 100644
index 00000000000..dc4ceacddb1
--- /dev/null
+++ b/db/migrate/20160525205328_remove_main_language_from_projects.rb
@@ -0,0 +1,22 @@
+# rubocop:disable all
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveMainLanguageFromProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_column :projects, :main_language
+ end
+end
diff --git a/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb
new file mode 100644
index 00000000000..3e26be7c09c
--- /dev/null
+++ b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb
@@ -0,0 +1,14 @@
+# rubocop:disable all
+class RemoveNotificationSettingsForDeletedProjects < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM notification_settings
+ WHERE notification_settings.source_type = 'Project'
+ AND NOT EXISTS (
+ SELECT *
+ FROM projects
+ WHERE projects.id = notification_settings.source_id
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20160528043124_add_users_state_index.rb b/db/migrate/20160528043124_add_users_state_index.rb
new file mode 100644
index 00000000000..6419d2ae71d
--- /dev/null
+++ b/db/migrate/20160528043124_add_users_state_index.rb
@@ -0,0 +1,10 @@
+# rubocop:disable all
+class AddUsersStateIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :users, :state
+ end
+end
diff --git a/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb
new file mode 100644
index 00000000000..d811fd5271e
--- /dev/null
+++ b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb
@@ -0,0 +1,10 @@
+# rubocop:disable all
+# This is ONLINE migration
+
+class AddContainerRegistryTokenExpireDelayToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :application_settings, :container_registry_token_expire_delay, :integer, default: 5
+ end
+end
diff --git a/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb
new file mode 100644
index 00000000000..be295f0181d
--- /dev/null
+++ b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddHasExternalIssueTrackerToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column(:projects, :has_external_issue_tracker, :boolean)
+ end
+end
diff --git a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb
new file mode 100644
index 00000000000..4f4f58b1619
--- /dev/null
+++ b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb
@@ -0,0 +1,33 @@
+# rubocop:disable all
+class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration
+ def up
+ duplicates = exec_query(%Q{
+ SELECT user_id, source_type, source_id
+ FROM notification_settings
+ GROUP BY user_id, source_type, source_id
+ HAVING COUNT(*) > 1
+ })
+
+ duplicates.each do |row|
+ uid = row['user_id']
+ stype = connection.quote(row['source_type'])
+ sid = row['source_id']
+
+ execute(%Q{
+ DELETE FROM notification_settings
+ WHERE user_id = #{uid}
+ AND source_type = #{stype}
+ AND source_id = #{sid}
+ AND id != (
+ SELECT id FROM (
+ SELECT min(id) AS id
+ FROM notification_settings
+ WHERE user_id = #{uid}
+ AND source_type = #{stype}
+ AND source_id = #{sid}
+ ) min_ids
+ )
+ })
+ end
+ end
+end
diff --git a/db/migrate/20160603182247_add_index_to_notification_settings.rb b/db/migrate/20160603182247_add_index_to_notification_settings.rb
new file mode 100644
index 00000000000..f6ae26d555f
--- /dev/null
+++ b/db/migrate/20160603182247_add_index_to_notification_settings.rb
@@ -0,0 +1,10 @@
+# rubocop:disable all
+class AddIndexToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :notification_settings, [:user_id, :source_id, :source_type], { unique: true, name: "index_notifications_on_user_id_and_source_id_and_source_type" }
+ end
+end
diff --git a/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb
new file mode 100644
index 00000000000..3c5d2ad910e
--- /dev/null
+++ b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb
@@ -0,0 +1,6 @@
+# rubocop:disable all
+class AddAfterSignUpTextToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :after_sign_up_text, :text
+ end
+end
diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
new file mode 100644
index 00000000000..c700d2b569d
--- /dev/null
+++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
@@ -0,0 +1,12 @@
+class AddRepositoryStorageToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects, :repository_storage, :string, default: 'default')
+ end
+
+ def down
+ remove_column(:projects, :repository_storage)
+ end
+end
diff --git a/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb b/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb
new file mode 100644
index 00000000000..34c702e3fa6
--- /dev/null
+++ b/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb
@@ -0,0 +1,13 @@
+class AddUserDefaultExternalToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :user_default_external, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :user_default_external)
+ end
+end
diff --git a/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb b/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb
new file mode 100644
index 00000000000..259abb08e47
--- /dev/null
+++ b/db/migrate/20160610140403_remove_notification_setting_not_null_constraints.rb
@@ -0,0 +1,11 @@
+class RemoveNotificationSettingNotNullConstraints < ActiveRecord::Migration
+ def up
+ change_column :notification_settings, :source_type, :string, null: true
+ change_column :notification_settings, :source_id, :integer, null: true
+ end
+
+ def down
+ change_column :notification_settings, :source_type, :string, null: false
+ change_column :notification_settings, :source_id, :integer, null: false
+ end
+end
diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
new file mode 100644
index 00000000000..477b2106dea
--- /dev/null
+++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
@@ -0,0 +1,6 @@
+class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration
+ def change
+ remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false
+ remove_column :projects, :issues_tracker_id, :string
+ end
+end
diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb
new file mode 100644
index 00000000000..760b766828e
--- /dev/null
+++ b/db/migrate/20160610201627_migrate_users_notification_level.rb
@@ -0,0 +1,21 @@
+class MigrateUsersNotificationLevel < ActiveRecord::Migration
+ # Migrates only users who changed their default notification level :participating
+ # creating a new record on notification settings table
+
+ def up
+ execute(%Q{
+ INSERT INTO notification_settings
+ (user_id, level, created_at, updated_at)
+ (SELECT id, notification_level, created_at, updated_at FROM users WHERE notification_level != 1)
+ })
+ end
+
+ # Migrates from notification settings back to user notification_level
+ # If no value is found the default level of 1 will be used
+ def down
+ execute(%Q{
+ UPDATE users u SET
+ notification_level = COALESCE((SELECT level FROM notification_settings WHERE user_id = u.id AND source_type IS NULL), 1)
+ })
+ end
+end
diff --git a/db/migrate/20160610204157_add_deployments.rb b/db/migrate/20160610204157_add_deployments.rb
new file mode 100644
index 00000000000..cb144ea8a6d
--- /dev/null
+++ b/db/migrate/20160610204157_add_deployments.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ create_table :deployments, force: true do |t|
+ t.integer :iid, null: false
+ t.integer :project_id, null: false
+ t.integer :environment_id, null: false
+ t.string :ref, null: false
+ t.boolean :tag, null: false
+ t.string :sha, null: false
+ t.integer :user_id
+ t.integer :deployable_id
+ t.string :deployable_type
+ t.datetime :created_at
+ t.datetime :updated_at
+ end
+
+ add_index :deployments, :project_id
+ add_index :deployments, [:project_id, :iid], unique: true
+ add_index :deployments, [:project_id, :environment_id]
+ add_index :deployments, [:project_id, :environment_id, :iid]
+ end
+end
diff --git a/db/migrate/20160610204158_add_environments.rb b/db/migrate/20160610204158_add_environments.rb
new file mode 100644
index 00000000000..e1c71d173c4
--- /dev/null
+++ b/db/migrate/20160610204158_add_environments.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ create_table :environments, force: true do |t|
+ t.integer :project_id, null: false
+ t.string :name, null: false
+ t.datetime :created_at
+ t.datetime :updated_at
+ end
+
+ add_index :environments, [:project_id, :name]
+ end
+end
diff --git a/db/migrate/20160610211845_add_environment_to_builds.rb b/db/migrate/20160610211845_add_environment_to_builds.rb
new file mode 100644
index 00000000000..990e445ac55
--- /dev/null
+++ b/db/migrate/20160610211845_add_environment_to_builds.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :ci_builds, :environment, :string
+ end
+end
diff --git a/db/migrate/20160610301627_remove_notification_level_from_users.rb b/db/migrate/20160610301627_remove_notification_level_from_users.rb
new file mode 100644
index 00000000000..8afb14df2cf
--- /dev/null
+++ b/db/migrate/20160610301627_remove_notification_level_from_users.rb
@@ -0,0 +1,7 @@
+class RemoveNotificationLevelFromUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ remove_column :users, :notification_level, :integer
+ end
+end
diff --git a/db/migrate/20160614182521_add_repository_storage_to_application_settings.rb b/db/migrate/20160614182521_add_repository_storage_to_application_settings.rb
new file mode 100644
index 00000000000..6dae91b700b
--- /dev/null
+++ b/db/migrate/20160614182521_add_repository_storage_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddRepositoryStorageToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :repository_storage, :string, default: 'default'
+ end
+end
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
new file mode 100644
index 00000000000..63f7392e54f
--- /dev/null
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -0,0 +1,9 @@
+class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :members, :requested_at
+ end
+end
diff --git a/db/migrate/20160615173316_add_enabled_git_access_protocols_to_application_settings.rb b/db/migrate/20160615173316_add_enabled_git_access_protocols_to_application_settings.rb
new file mode 100644
index 00000000000..013904b3f4f
--- /dev/null
+++ b/db/migrate/20160615173316_add_enabled_git_access_protocols_to_application_settings.rb
@@ -0,0 +1,11 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+# rubocop:disable all
+
+class AddEnabledGitAccessProtocolsToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :application_settings, :enabled_git_access_protocol, :string
+ end
+end
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
new file mode 100644
index 00000000000..bd0463886bc
--- /dev/null
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -0,0 +1,9 @@
+class SetMissingStageOnCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ update_column_in_batches(:ci_builds, :stage, :test) do |table, query|
+ query.where(table[:stage].eq(nil))
+ end
+ end
+end
diff --git a/db/migrate/20160616084004_change_project_of_environment.rb b/db/migrate/20160616084004_change_project_of_environment.rb
new file mode 100644
index 00000000000..cc1daf9b621
--- /dev/null
+++ b/db/migrate/20160616084004_change_project_of_environment.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ChangeProjectOfEnvironment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ change_column_null :environments, :project_id, true
+ end
+end
diff --git a/db/migrate/20160616102642_remove_duplicated_keys.rb b/db/migrate/20160616102642_remove_duplicated_keys.rb
new file mode 100644
index 00000000000..00a45d7fe73
--- /dev/null
+++ b/db/migrate/20160616102642_remove_duplicated_keys.rb
@@ -0,0 +1,19 @@
+# rubocop:disable all
+class RemoveDuplicatedKeys < ActiveRecord::Migration
+ def up
+ select_all("SELECT fingerprint FROM #{quote_table_name(:keys)} GROUP BY fingerprint HAVING COUNT(*) > 1").each do |row|
+ fingerprint = connection.quote(row['fingerprint'])
+ execute(%Q{
+ DELETE FROM keys
+ WHERE fingerprint = #{fingerprint}
+ AND id != (
+ SELECT id FROM (
+ SELECT max(id) AS id
+ FROM keys
+ WHERE fingerprint = #{fingerprint}
+ ) max_ids
+ )
+ })
+ end
+ end
+end
diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
new file mode 100644
index 00000000000..4bb4204cebd
--- /dev/null
+++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
@@ -0,0 +1,21 @@
+class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/250
+ # That MR was added on gitlab-ee so we need to check if the index
+ # already exists because we want to do is create an unique index instead.
+
+ def up
+ if index_exists?(:keys, :fingerprint)
+ remove_index :keys, :fingerprint
+ end
+ end
+
+ def down
+ unless index_exists?(:keys, :fingerprint)
+ add_concurrent_index :keys, :fingerprint
+ end
+ end
+end
diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
new file mode 100644
index 00000000000..e35af38aac3
--- /dev/null
+++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
@@ -0,0 +1,13 @@
+class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :keys, :fingerprint, unique: true
+ end
+
+ def down
+ remove_index :keys, :fingerprint
+ end
+end
diff --git a/db/migrate/20160617301627_add_events_to_notification_settings.rb b/db/migrate/20160617301627_add_events_to_notification_settings.rb
new file mode 100644
index 00000000000..609596f45e4
--- /dev/null
+++ b/db/migrate/20160617301627_add_events_to_notification_settings.rb
@@ -0,0 +1,7 @@
+class AddEventsToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :notification_settings, :events, :text
+ end
+end
diff --git a/db/migrate/20160620110927_fix_no_validatable_import_url.rb b/db/migrate/20160620110927_fix_no_validatable_import_url.rb
new file mode 100644
index 00000000000..a3f5073d511
--- /dev/null
+++ b/db/migrate/20160620110927_fix_no_validatable_import_url.rb
@@ -0,0 +1,106 @@
+# Updates project records containing invalid URLs using the AddressableUrlValidator.
+# This is optimized assuming the number of invalid records is low, but
+# we still need to loop through all the projects with an +import_url+
+# so we use batching for the latter.
+#
+# This migration is non-reversible as we would have to keep the old data.
+
+class FixNoValidatableImportUrl < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ class SqlBatches
+
+ attr_reader :results, :query
+
+ def initialize(batch_size: 1000, query:)
+ @offset = 0
+ @batch_size = batch_size
+ @query = query
+ @results = []
+ end
+
+ def next?
+ @results = ActiveRecord::Base.connection.exec_query(batched_sql)
+ @offset += @batch_size
+ @results.any?
+ end
+
+ private
+
+ def batched_sql
+ "#{@query} LIMIT #{@batch_size} OFFSET #{@offset}"
+ end
+ end
+
+ # AddressableValidator - Snapshot of AddressableUrlValidator
+ module AddressableUrlValidatorSnap
+ extend self
+
+ def valid_url?(value)
+ return false unless value
+
+ valid_uri?(value) && valid_protocol?(value)
+ rescue Addressable::URI::InvalidURIError
+ false
+ end
+
+ def valid_uri?(value)
+ Addressable::URI.parse(value).is_a?(Addressable::URI)
+ end
+
+ def valid_protocol?(value)
+ value =~ /\A#{URI.regexp(%w(http https ssh git))}\z/
+ end
+ end
+
+ def up
+ unless defined?(Addressable::URI::InvalidURIError)
+ say('Skipping cleaning up invalid import URLs as class from Addressable is missing')
+ return
+ end
+
+ say('Nullifying empty import URLs')
+
+ nullify_empty_urls
+
+ say('Cleaning up invalid import URLs... This may take a few minutes if we have a large number of imported projects.')
+
+ process_invalid_import_urls
+ end
+
+ def process_invalid_import_urls
+ batches = SqlBatches.new(query: "SELECT id, import_url FROM projects WHERE import_url IS NOT NULL")
+
+ while batches.next?
+ project_ids = []
+
+ batches.results.each do |result|
+ project_ids << result['id'] unless valid_url?(result['import_url'])
+ end
+
+ process_batch(project_ids)
+ end
+
+ end
+
+ def process_batch(project_ids)
+ Thread.new do
+ begin
+ project_ids.each { |project_id| cleanup_import_url(project_id) }
+ ensure
+ ActiveRecord::Base.connection.close
+ end
+ end.join
+ end
+
+ def valid_url?(url)
+ AddressableUrlValidatorSnap.valid_url?(url)
+ end
+
+ def cleanup_import_url(project_id)
+ execute("UPDATE projects SET import_url = NULL WHERE id = #{project_id}")
+ end
+
+ def nullify_empty_urls
+ execute("UPDATE projects SET import_url = NULL WHERE import_url = ''")
+ end
+end
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
new file mode 100644
index 00000000000..dfa5110dea4
--- /dev/null
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexOnRunnersLocked < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_runners, :locked
+ end
+end
diff --git a/db/migrate/20160628085157_add_artifacts_size_to_ci_builds.rb b/db/migrate/20160628085157_add_artifacts_size_to_ci_builds.rb
new file mode 100644
index 00000000000..61dd726fac7
--- /dev/null
+++ b/db/migrate/20160628085157_add_artifacts_size_to_ci_builds.rb
@@ -0,0 +1,7 @@
+class AddArtifactsSizeToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column(:ci_builds, :artifacts_size, :integer)
+ end
+end
diff --git a/db/migrate/20160703180340_add_index_on_award_emoji_user_and_name.rb b/db/migrate/20160703180340_add_index_on_award_emoji_user_and_name.rb
new file mode 100644
index 00000000000..0c25f87dfb4
--- /dev/null
+++ b/db/migrate/20160703180340_add_index_on_award_emoji_user_and_name.rb
@@ -0,0 +1,11 @@
+# rubocop:disable all
+# Migration type: online without errors
+
+class AddIndexOnAwardEmojiUserAndName < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:award_emoji, [:user_id, :name])
+ end
+end
diff --git a/db/migrate/20160705163108_remove_requesters_that_are_owners.rb b/db/migrate/20160705163108_remove_requesters_that_are_owners.rb
new file mode 100644
index 00000000000..1fca230c019
--- /dev/null
+++ b/db/migrate/20160705163108_remove_requesters_that_are_owners.rb
@@ -0,0 +1,40 @@
+class RemoveRequestersThatAreOwners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ # Delete requesters that are owner of their projects and actually requested
+ # access to it
+ execute <<-SQL
+ DELETE FROM members
+ WHERE members.source_type = 'Project'
+ AND members.type = 'ProjectMember'
+ AND members.requested_at IS NOT NULL
+ AND members.user_id = (
+ SELECT namespaces.owner_id
+ FROM namespaces
+ JOIN projects ON namespaces.id = projects.namespace_id
+ WHERE namespaces.type IS NULL
+ AND projects.id = members.source_id
+ AND namespaces.owner_id = members.user_id);
+ SQL
+
+ # Delete requesters that are owner of their project's group and actually requested
+ # access to it
+ execute <<-SQL
+ DELETE FROM members
+ WHERE members.source_type = 'Project'
+ AND members.type = 'ProjectMember'
+ AND members.requested_at IS NOT NULL
+ AND members.user_id = (
+ SELECT namespaces.owner_id
+ FROM namespaces
+ JOIN projects ON namespaces.id = projects.namespace_id
+ WHERE namespaces.type = 'Group'
+ AND projects.id = members.source_id
+ AND namespaces.owner_id = members.user_id);
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 14d7e84d856..be3501c4c2e 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -1,3 +1,4 @@
+# rubocop:disable all
class LimitsToMysql < ActiveRecord::Migration
def up
return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
diff --git a/db/schema.rb b/db/schema.rb
index 71d953afe30..a5eea3a697c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160508194200) do
+ActiveRecord::Schema.define(version: 20160705163108) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -43,43 +43,51 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
+ t.integer "default_branch_protection", default: 2
t.text "restricted_visibility_levels"
- t.boolean "version_check_enabled", default: true
- t.integer "max_attachment_size", default: 10, null: false
+ t.boolean "version_check_enabled", default: true
+ t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility"
t.integer "default_snippet_visibility"
t.text "restricted_signup_domains"
- t.boolean "user_oauth_applications", default: true
+ t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
+ t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
t.string "admin_notification_email"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.integer "max_artifacts_size", default: 100, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
- t.boolean "require_two_factor_authentication", default: false
- t.integer "two_factor_grace_period", default: 48
- t.boolean "metrics_enabled", default: false
- t.string "metrics_host", default: "localhost"
- t.integer "metrics_pool_size", default: 16
- t.integer "metrics_timeout", default: 10
- t.integer "metrics_method_call_threshold", default: 10
- t.boolean "recaptcha_enabled", default: false
+ t.boolean "require_two_factor_authentication", default: false
+ t.integer "two_factor_grace_period", default: 48
+ t.boolean "metrics_enabled", default: false
+ t.string "metrics_host", default: "localhost"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
t.string "recaptcha_site_key"
t.string "recaptcha_private_key"
- t.integer "metrics_port", default: 8089
- t.boolean "akismet_enabled", default: false
+ t.integer "metrics_port", default: 8089
+ t.boolean "akismet_enabled", default: false
t.string "akismet_api_key"
- t.integer "metrics_sample_interval", default: 15
- t.boolean "sentry_enabled", default: false
+ t.integer "metrics_sample_interval", default: 15
+ t.boolean "sentry_enabled", default: false
t.string "sentry_dsn"
- t.boolean "email_author_in_body", default: false
+ t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
- t.boolean "repository_checks_enabled", default: false
+ t.boolean "repository_checks_enabled", default: false
t.text "shared_runners_text"
- t.integer "metrics_packet_size", default: 1
+ t.integer "metrics_packet_size", default: 1
+ t.text "disabled_oauth_sign_in_sources"
+ t.string "health_check_access_token"
+ t.boolean "send_user_confirmation_email", default: false
+ t.integer "container_registry_token_expire_delay", default: 5
+ t.boolean "user_default_external", default: false, null: false
+ t.text "after_sign_up_text"
+ t.string "repository_storage", default: "default"
+ t.string "enabled_git_access_protocol"
end
create_table "audit_events", force: :cascade do |t|
@@ -96,6 +104,19 @@ ActiveRecord::Schema.define(version: 20160508194200) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
+ create_table "award_emoji", force: :cascade do |t|
+ t.string "name"
+ t.integer "user_id"
+ t.integer "awardable_id"
+ t.string "awardable_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
+ add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
+ add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
+
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at"
@@ -127,9 +148,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
t.integer "stage_idx"
@@ -144,6 +165,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
+ t.string "environment"
+ t.datetime "artifacts_expire_at"
+ t.integer "artifacts_size"
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
@@ -267,9 +291,12 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.string "revision"
t.string "platform"
t.string "architecture"
+ t.boolean "run_untagged", default: true, null: false
+ t.boolean "locked", default: false, null: false
end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
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"}
@@ -363,6 +390,25 @@ ActiveRecord::Schema.define(version: 20160508194200) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
+ create_table "deployments", force: :cascade do |t|
+ t.integer "iid", null: false
+ t.integer "project_id", null: false
+ t.integer "environment_id", null: false
+ t.string "ref", null: false
+ t.boolean "tag", null: false
+ t.string "sha", null: false
+ t.integer "user_id"
+ t.integer "deployable_id"
+ t.string "deployable_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
+ add_index "deployments", ["project_id", "environment_id"], name: "index_deployments_on_project_id_and_environment_id", using: :btree
+ add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
+ add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
+
create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
@@ -373,6 +419,15 @@ ActiveRecord::Schema.define(version: 20160508194200) do
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
+ create_table "environments", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "name", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+
create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
@@ -459,6 +514,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
+ add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: :cascade do |t|
@@ -480,8 +536,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.datetime "updated_at"
t.boolean "template", default: false
t.string "description"
+ t.integer "priority"
end
+ add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
@@ -516,11 +574,13 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.string "invite_email"
t.string "invite_token"
t.datetime "invite_accepted_at"
+ t.datetime "requested_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
+ add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
@@ -534,6 +594,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.datetime "updated_at"
t.string "base_commit_sha"
t.string "real_size"
+ t.string "head_commit_sha"
+ t.string "start_commit_sha"
end
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
@@ -630,17 +692,18 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.string "line_code"
t.string "commit_id"
t.integer "noteable_id"
- t.boolean "system", default: false, null: false
+ t.boolean "system", default: false, null: false
t.text "st_diff"
t.integer "updated_by_id"
- t.boolean "is_award", default: false, null: false
+ t.string "type"
+ t.text "position"
+ t.text "original_position"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
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
@@ -651,14 +714,16 @@ ActiveRecord::Schema.define(version: 20160508194200) do
create_table "notification_settings", force: :cascade do |t|
t.integer "user_id", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
+ t.integer "source_id"
+ t.string "source_type"
t.integer "level", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.text "events"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
+ add_index "notification_settings", ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree
add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
create_table "oauth_access_grants", force: :cascade do |t|
@@ -704,6 +769,19 @@ ActiveRecord::Schema.define(version: 20160508194200) 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 "personal_access_tokens", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "revoked", default: false
+ t.datetime "expires_at"
+ end
+
+ add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -727,38 +805,39 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
- t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
- t.integer "commit_count", default: 0
+ t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
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
+ 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.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
+ t.boolean "container_registry_enabled"
+ t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "has_external_issue_tracker"
+ t.string "repository_storage", default: "default", null: false
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -807,6 +886,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.string "commit_id"
t.string "reply_key", null: false
t.string "line_code"
+ t.string "note_type"
+ t.text "position"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
@@ -924,6 +1005,19 @@ ActiveRecord::Schema.define(version: 20160508194200) do
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 "u2f_registrations", force: :cascade do |t|
+ t.text "certificate"
+ t.string "key_handle"
+ t.string "public_key"
+ t.integer "counter"
+ t.integer "user_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
+ add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_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
@@ -953,7 +1047,6 @@ ActiveRecord::Schema.define(version: 20160508194200) do
t.boolean "can_create_team", default: true, null: false
t.string "state"
t.integer "color_scheme_id", default: 1, null: false
- t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
@@ -995,6 +1088,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do
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", ["state"], name: "index_users_on_state", 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"}
@@ -1030,4 +1124,6 @@ ActiveRecord::Schema.define(version: 20160508194200) do
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index e358da1c424..cf7a828d91e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,12 +3,14 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
-- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples.
+- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
+- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
+- [Importing and exporting projects between instances](user/project/settings/import_export.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
+- [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 (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.
@@ -19,19 +21,21 @@
## Administrator documentation
+- [Access restrictions](administration/access_restrictions.md) Define which Git access protocols can be used to talk to GitLab
- [Authentication/Authorization](administration/auth/README.md) Configure
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
-- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
+- [Custom Git hooks](administration/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
+- [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, 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.
+- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Operations](operations/README.md) Keeping GitLab up and running
+- [Operations](operations/README.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
-- [Repository checks](administration/repository_checks.md) Periodic Git repository checks
+- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
+- [Repository storages](administration/repository_storages.md) Manage the paths used to store repositories.
- [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.
@@ -40,9 +44,12 @@
- [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
-- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs
-- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability
+- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
+- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
+- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
+- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
## Contributor documentation
diff --git a/doc/administration/access_restrictions.md b/doc/administration/access_restrictions.md
new file mode 100644
index 00000000000..51d7996effd
--- /dev/null
+++ b/doc/administration/access_restrictions.md
@@ -0,0 +1,38 @@
+# Access Restrictions
+
+> **Note:** This feature is only available on versions 8.10 and above.
+
+With GitLab's Access restrictions you can choose which Git access protocols you
+want your users to use to communicate with GitLab. This feature can be enabled
+via the `Application Settings` in the Admin interface.
+
+The setting is called `Enabled Git access protocols`, and it gives you the option
+to choose between:
+
+- Both SSH and HTTP(S)
+- Only SSH
+- Only HTTP(s)
+
+![Settings Overview](img/access_restrictions.png)
+
+## Enabled Protocol
+
+When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give
+your users the option to choose which protocol they would like to use.
+
+When you choose to allow only one of the protocols, a couple of things will happen:
+
+- The project page will only show the allowed protocol's URL, with no option to
+ change it.
+- A tooltip will be shown when you hover over the URL's protocol, if an action
+ on the user's part is required, e.g. adding an SSH key, or setting a password.
+
+![Project URL with SSH only access](img/restricted_url.png)
+
+On top of these UI restrictions, GitLab will deny all Git actions on the protocol
+not selected.
+
+> **Note:** Please keep in mind that disabling an access protocol does not actually
+ block access to the server itself. The ports used for the protocol, be it SSH or
+ HTTP, will still be accessible. What GitLab does is restrict access on the
+ application level. \ No newline at end of file
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 10096779844..7186f707ad6 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -130,27 +130,27 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
first_name: 'givenName'
last_name: 'sn'
- ## EE only
-
- # Base where we can search for groups
- #
- # Ex. ou=groups,dc=gitlab,dc=example
- #
- group_base: ''
-
- # The CN of a group containing GitLab administrators
- #
- # Ex. administrators
- #
- # Note: Not `cn=administrators` or the full DN
- #
- admin_group: ''
-
- # The LDAP attribute containing a user's public SSH key
- #
- # Ex. ssh_public_key
- #
- sync_ssh_keys: false
+ ## EE only
+
+ # Base where we can search for groups
+ #
+ # Ex. ou=groups,dc=gitlab,dc=example
+ #
+ group_base: ''
+
+ # The CN of a group containing GitLab administrators
+ #
+ # Ex. administrators
+ #
+ # Note: Not `cn=administrators` or the full DN
+ #
+ admin_group: ''
+
+ # The LDAP attribute containing a user's public SSH key
+ #
+ # Ex. ssh_public_key
+ #
+ sync_ssh_keys: false
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
new file mode 100644
index 00000000000..d5d43303454
--- /dev/null
+++ b/doc/administration/container_registry.md
@@ -0,0 +1,457 @@
+# GitLab Container Registry Administration
+
+> **Note:**
+This feature was [introduced][ce-4040] in GitLab 8.8.
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+<!-- 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)*
+
+- [Enable the Container Registry](#enable-the-container-registry)
+- [Container Registry domain configuration](#container-registry-domain-configuration)
+ - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain)
+ - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain)
+- [Disable Container Registry site-wide](#disable-container-registry-site-wide)
+- [Disable Container Registry per project](#disable-container-registry-per-project)
+- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
+- [Container Registry storage path](#container-registry-storage-path)
+- [Container Registry storage driver](#container-registry-storage-driver)
+- [Storage limitations](#storage-limitations)
+- [Changelog](#changelog)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+## Enable the Container Registry
+
+**Omnibus GitLab installations**
+
+All you have to do is configure the domain name under which the Container
+Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration)
+and pick one of the two options that fits your case.
+
+>**Note:**
+The container Registry works under HTTPS by default. Using HTTP is possible
+but not recommended and out of the scope of this document.
+Read the [insecure Registry documentation][docker-insecure] if you want to
+implement this.
+
+---
+
+**Installations from source**
+
+If you have installed GitLab from source:
+
+1. You will have to [install Docker Registry][registry-deploy] by yourself.
+1. After the installation is complete, you will have to configure the Registry's
+ settings in `gitlab.yml` in order to enable it.
+1. Use the sample NGINX configuration file that is found under
+ [`lib/support/nginx/registry-ssl`][registry-ssl] and edit it to match the
+ `host`, `port` and TLS certs paths.
+
+The contents of `gitlab.yml` are:
+
+```
+registry:
+ enabled: true
+ host: registry.gitlab.example.com
+ port: 5005
+ api_url: http://localhost:5000/
+ key: config/registry.key
+ path: shared/registry
+ issuer: gitlab-issuer
+```
+
+where:
+
+| Parameter | Description |
+| --------- | ----------- |
+| `enabled` | `true` or `false`. Enables the Registry in GitLab. By default this is `false`. |
+| `host` | The host URL under which the Registry will run and the users will be able to use. |
+| `port` | The port under which the external Registry domain will listen on. |
+| `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. |
+| `key` | The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. |
+| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). |
+| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
+
+>**Note:**
+GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab]
+will not restart the Registry should you modify its settings. Read the upstream
+documentation on how to achieve that.
+
+The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+
+```
+auth:
+ token:
+ realm: https://gitlab.example.com/jwt/auth
+ service: container_registry
+ issuer: gitlab-issuer
+ rootcertbundle: /root/certs/certbundle
+```
+
+## Container Registry domain configuration
+
+There are two ways you can configure the Registry's external domain.
+
+- Either [use the existing GitLab domain][existing-domain] where in that case
+ the Registry will have to listen on a port and reuse GitLab's TLS certificate,
+- or [use a completely separate domain][new-domain] with a new TLS certificate
+ for that domain.
+
+Since the container Registry requires a TLS certificate, in the end it all boils
+down to how easy or pricey is to get a new one.
+
+Please take this into consideration before configuring the Container Registry
+for the first time.
+
+### Configure Container Registry under an existing GitLab domain
+
+If the Registry is configured to use the existing GitLab domain, you can
+expose the Registry on a port so that you can reuse the existing GitLab TLS
+certificate.
+
+Assuming that the GitLab domain is `https://gitlab.example.com` and the port the
+Registry is exposed to the outside world is `4567`, here is what you need to set
+in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed
+GitLab from source respectively.
+
+---
+
+**Omnibus GitLab installations**
+
+1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the
+ path to the existing TLS certificate and key used by GitLab:
+
+ ```ruby
+ registry_external_url 'https://gitlab.example.com:4567'
+ ```
+
+ Note how the `registry_external_url` is listening on HTTPS under the
+ existing GitLab URL, but on a different port.
+
+ If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt`
+ and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines
+ below:
+
+ ```ruby
+ registry_nginx['ssl_certificate'] = "/path/to/certificate.pem"
+ registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
+ configure it with the following settings:
+
+ ```
+ registry:
+ enabled: true
+ host: gitlab.example.com
+ port: 4567
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path).
+
+---
+
+Users should now be able to login to the Container Registry with their GitLab
+credentials using:
+
+```bash
+docker login gitlab.example.com:4567
+```
+
+### Configure Container Registry under its own domain
+
+If the Registry is configured to use its own domain, you will need a TLS
+certificate for that specific domain (e.g., `registry.example.com`) or maybe
+a wildcard certificate if hosted under a subdomain of your existing GitLab
+domain (e.g., `registry.gitlab.example.com`).
+
+Let's assume that you want the container Registry to be accessible at
+`https://registry.gitlab.example.com`.
+
+---
+
+**Omnibus GitLab installations**
+
+1. Place your TLS certificate and key in
+ `/etc/gitlab/ssl/registry.gitlab.example.com.crt` and
+ `/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have
+ correct permissions:
+
+ ```bash
+ chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.*
+ ```
+
+1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with:
+
+ ```ruby
+ registry_external_url 'https://registry.gitlab.example.com'
+ ```
+
+ Note how the `registry_external_url` is listening on HTTPS.
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+> **Note:**
+If you have a [wildcard certificate][], you need to specify the path to the
+certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will
+look like:
+>
+```ruby
+registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem"
+registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key"
+```
+
+---
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
+ configure it with the following settings:
+
+ ```
+ registry:
+ enabled: true
+ host: registry.gitlab.example.com
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path).
+
+---
+
+Users should now be able to login to the Container Registry using their GitLab
+credentials:
+
+```bash
+docker login registry.gitlab.example.com
+```
+
+## Disable Container Registry site-wide
+
+>**Note:**
+Disabling the Registry in the Rails GitLab application as set by the following
+steps, will not remove any existing Docker images. This is handled by the
+Registry application itself.
+
+**Omnibus GitLab**
+
+1. Open `/etc/gitlab/gitlab.rb` and set `registry['enable']` to `false`:
+
+ ```ruby
+ registry['enable'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
+ set `enabled` to `false`:
+
+ ```
+ registry:
+ enabled: false
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Disable Container Registry per project
+
+If Registry is enabled in your GitLab instance, but you don't need it for your
+project, you can disable it from your project's settings. Read the user guide
+on how to achieve that.
+
+## Disable Container Registry for new projects site-wide
+
+If the Container Registry is enabled, then it will be available on all new
+projects. To disable this function and let the owners of a project to enable
+the Container Registry by themselves, follow the steps below.
+
+---
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['gitlab_default_projects_features_container_registry'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, find the `default_projects_features`
+ entry and configure it so that `container_registry` is set to `false`:
+
+ ```
+ ## Default project features settings
+ default_projects_features:
+ issues: true
+ merge_requests: true
+ wiki: true
+ snippets: false
+ builds: true
+ container_registry: false
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Container Registry storage path
+
+>**Note:**
+For configuring storage in the cloud instead of the filesystem, see the
+[storage driver configuration](#container-registry-storage-driver).
+
+If you want to store your images on the filesystem, you can change the storage
+path for the Container Registry, follow the steps below.
+
+This path is accessible to:
+
+- the user running the Container Registry daemon,
+- the user running GitLab
+
+> **Warning** You should confirm that all GitLab, Registry and web server users
+have access to this directory.
+
+---
+
+**Omnibus GitLab installations**
+
+The default location where images are stored in Omnibus, is
+`/var/opt/gitlab/gitlab-rails/shared/registry`. To change it:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['registry_path'] = "/path/to/registry/storage"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+The default location where images are stored in source installations, is
+`/home/git/gitlab/shared/registry`. To change it:
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
+ change the `path` setting:
+
+ ```
+ registry:
+ path: shared/registry
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Container Registry storage driver
+
+You can configure the Container Registry to use a different storage backend by
+configuring a different storage driver. By default the GitLab Container Registry
+is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
+configuration.
+
+The different supported drivers are:
+
+| Driver | Description |
+|------------|-------------------------------------|
+| filesystem | Uses a path on the local filesystem |
+| azure | Microsoft Azure Blob Storage |
+| gcs | Google Cloud Storage |
+| s3 | Amazon Simple Storage Service |
+| swift | OpenStack Swift Object Storage |
+| oss | Aliyun OSS |
+
+Read more about the individual driver's config options in the
+[Docker Registry docs][storage-config].
+
+> **Warning** GitLab will not backup Docker images that are not stored on the
+filesystem. Remember to enable backups with your object storage provider if
+desired.
+
+---
+
+**Omnibus GitLab installations**
+
+To configure the storage driver in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['storage'] = {
+ 's3' => {
+ 'accesskey' => 's3-access-key',
+ 'secretkey' => 's3-secret-key-for-access-key',
+ 'bucket' => 'your-s3-bucket'
+ }
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the storage driver is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+storage:
+ s3:
+ accesskey: 'AKIAKIAKI'
+ secretkey: 'secret123'
+ bucket: 'gitlab-registry-bucket-AKIAKIAKI'
+ cache:
+ blobdescriptor: inmemory
+ delete:
+ enabled: true
+```
+
+## Storage limitations
+
+Currently, there is no storage limitation, which means a user can upload an
+infinite amount of Docker images with arbitrary sizes. This setting will be
+configurable in future releases.
+
+## Changelog
+
+**GitLab 8.8 ([source docs][8-8-docs])**
+
+- GitLab Container Registry feature was introduced.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart gitlab]: restart_gitlab.md#installations-from-source
+[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate
+[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
+[docker-insecure]: https://docs.docker.com/registry/insecure/
+[registry-deploy]: https://docs.docker.com/registry/deploying/
+[storage-config]: https://docs.docker.com/registry/configuration/#storage
+[token-config]: https://docs.docker.com/registry/configuration/#token
+[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
+[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
+[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
+[new-domain]: #configure-container-registry-under-its-own-domain
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
new file mode 100644
index 00000000000..9fd7b71d2dc
--- /dev/null
+++ b/doc/administration/custom_hooks.md
@@ -0,0 +1,56 @@
+# Custom Git Hooks
+
+>
+**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](../web_hooks/web_hooks.md) as an option if you do not
+have filesystem access. For a user configurable Git hook interface, please see
+[GitLab Enterprise Edition Git Hooks](http://docs.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.
+See [Git SCM Server-Side Hooks][hooks] for more information about each hook type.
+
+As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
+administrators can add custom git hooks to any GitLab project.
+
+## Setup
+
+Normally, Git hooks are placed in the repository or project's `hooks` directory.
+GitLab creates a symlink from each project's `hooks` directory to the
+gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell
+upgrades. As such, custom hooks are implemented a little differently. Behavior
+is exactly the same once the hook is created, though.
+
+Follow the steps below to set up a custom hook:
+
+1. Pick a project that needs a custom Git hook.
+1. On the GitLab server, navigate to the project's repository directory.
+ For an installation from source the path is usually
+ `/home/git/repositories/<group>/<project>.git`. For Omnibus installs the path is
+ usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`.
+1. Create a new directory in this location called `custom_hooks`.
+1. Inside the new `custom_hooks` directory, create a file with a name matching
+ the hook type. For a pre-receive hook the file name should be `pre-receive`
+ with no extension.
+1. Make the hook file executable and make sure it's owned by git.
+1. Write the code to make the Git hook function as expected. Hooks can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+
+That's it! Assuming the hook code is properly implemented the hook will fire
+as appropriate.
+
+## Custom error messages
+
+>**Note:**
+This feature was [introduced][5073] in GitLab 8.10.
+
+If the commit is declined or an error occurs during the Git hook check,
+the STDERR and/or SDOUT message of the hook will be present in GitLab's UI.
+
+![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
+
+[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 43ab153d76d..7f53915a4d7 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -58,4 +58,4 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`.
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).
+For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container).
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 43d85ffb775..d74a786ac24 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -19,6 +19,8 @@ Components/Servers Required:
- 2 servers/virtual machines (one active/one passive)
+![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
+
### Active/Active
This architecture scales easily because all application servers handle
@@ -26,6 +28,8 @@ user requests simultaneously. The database, Redis, and GitLab application are
all deployed on separate servers. The configuration is **only** highly-available
if the database, Redis and storage are also configured as such.
+![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
+
**Steps to configure active/active:**
1. [Configure the database](database.md)
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index b1fe34ed9a1..136f570ac27 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -60,4 +60,4 @@ Read more on high-availability configuration:
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
-[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html
+[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index e4e124e200a..537f4f3501d 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -2,8 +2,8 @@
## Required NFS Server features
-**File locking**: GitLab **requires** file locking which is only supported
-natively in NFS version 4. NFSv3 also supports locking as long as
+**File locking**: GitLab **requires** advisory file locking, which is only
+supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3.
@@ -113,4 +113,4 @@ Read more on high-availability configuration:
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
-[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping"
+[udp-log-shipping]: http://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping"
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index d89a1e582ca..f6153216f33 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -26,7 +26,7 @@ that runs Redis.
```ruby
external_url 'https://gitlab.example.com'
- # Disable all components except PostgreSQL
+ # Disable all components except Redis
redis['enable'] = true
bootstrap['enable'] = false
nginx['enable'] = false
diff --git a/doc/administration/img/access_restrictions.png b/doc/administration/img/access_restrictions.png
new file mode 100644
index 00000000000..66fd9491e85
--- /dev/null
+++ b/doc/administration/img/access_restrictions.png
Binary files differ
diff --git a/doc/administration/img/custom_hooks_error_msg.png b/doc/administration/img/custom_hooks_error_msg.png
new file mode 100644
index 00000000000..92e87e15fb3
--- /dev/null
+++ b/doc/administration/img/custom_hooks_error_msg.png
Binary files differ
diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png
new file mode 100644
index 00000000000..81259e0ae93
--- /dev/null
+++ b/doc/administration/img/high_availability/active-active-diagram.png
Binary files differ
diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png
new file mode 100644
index 00000000000..f69ff1d0357
--- /dev/null
+++ b/doc/administration/img/high_availability/active-passive-diagram.png
Binary files differ
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
index f7c5bc44367..f72ad9a45d5 100644
--- a/doc/administration/img/housekeeping_settings.png
+++ b/doc/administration/img/housekeeping_settings.png
Binary files differ
diff --git a/doc/administration/img/restricted_url.png b/doc/administration/img/restricted_url.png
new file mode 100644
index 00000000000..0a677433dcf
--- /dev/null
+++ b/doc/administration/img/restricted_url.png
Binary files differ
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
new file mode 100644
index 00000000000..737b39db16c
--- /dev/null
+++ b/doc/administration/logs.md
@@ -0,0 +1,137 @@
+## Log system
+
+GitLab has an advanced log system where everything is logged so that you
+can analyze your instance using various system log files. In addition to
+system log files, GitLab Enterprise Edition comes with Audit Events.
+Find more about them [in Audit Events
+documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
+
+System log files are typically plain text in a standard log file format.
+This guide talks about how to read and use these system log files.
+
+### production.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
+omnibus package or in `/home/git/gitlab/log/production.log` for
+installations from source.
+
+It contains information about all performed requests. You can see the
+URL and type of request, IP address and what exactly parts of code were
+involved to service this particular request. Also you can see all SQL
+request that have been performed and how much time it took. This task is
+more useful for GitLab contributors and developers. Use part of this log
+file when you are going to report bug. For example:
+
+```
+Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
+Processing by Projects::TreeController#show as HTML
+ Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
+
+ ... [CUT OUT]
+
+ Namespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
+ CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
+ (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
+ Rendered layouts/nav/_project.html.haml (28.0ms)
+ Rendered layouts/_collapse_button.html.haml (0.2ms)
+ Rendered layouts/_flash.html.haml (0.1ms)
+ Rendered layouts/_page.html.haml (32.9ms)
+Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
+```
+
+In this example we can see that server processed an HTTP request with URL
+`/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12
+19:34:53 +0200. Also we can see that request was processed by
+`Projects::TreeController`.
+
+### application.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
+omnibus package or in `/home/git/gitlab/log/application.log` for
+installations from source.
+
+It helps you discover events happening in your instance such as user creation,
+project removing and so on. For example:
+
+```
+October 06, 2014 11:56: User "Administrator" (admin@example.com) was created
+October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore"
+October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce"
+October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed
+October 07, 2014 11:25: Project "project133" was removed
+```
+
+### githost.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
+omnibus package or in `/home/git/gitlab/log/githost.log` for
+installations from source.
+
+GitLab has to interact with Git repositories but in some rare cases
+something can go wrong and in this case you will know what exactly
+happened. This log file contains all failed requests from GitLab to Git
+repositories. In the majority of cases this file will be useful for developers
+only. For example:
+
+```
+December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict
+
+error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
+```
+
+### sidekiq.log
+
+This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
+omnibus package or in `/home/git/gitlab/log/sidekiq.log` for
+installations from source.
+
+GitLab uses background jobs for processing tasks which can take a long
+time. All information about processing these jobs are written down to
+this file. For example:
+
+```
+2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read'
+2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
+```
+
+### gitlab-shell.log
+
+This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
+omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for
+installations from source.
+
+GitLab shell is used by Gitlab for executing Git commands and provide
+SSH access to Git repositories. For example:
+
+```
+I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>.
+I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
+```
+
+### unicorn\_stderr.log
+
+This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
+omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for
+installations from source.
+
+Unicorn is a high-performance forking Web server which is used for
+serving the GitLab application. You can look at this log if, for
+example, your application does not respond. This log contains all
+information about the state of unicorn processes at any given time.
+
+```
+I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list
+I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12
+I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13
+I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready
+I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092
+I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready
+I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094
+I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready
+W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #<Unicorn::HttpServer:0x0000000208f618>: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes)
+W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1)
+I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1
+I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
+I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
+```
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
new file mode 100644
index 00000000000..c212059b9d5
--- /dev/null
+++ b/doc/administration/raketasks/project_import_export.md
@@ -0,0 +1,33 @@
+# Project import/export
+
+>**Note:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+The GitLab Import/Export version can be checked by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:version
+
+# Installations from source
+bundle exec rake gitlab:import_export:version RAILS_ENV=production
+```
+
+The current list of DB tables that will get exported can be listed by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:data
+
+# Installations from source
+bundle exec rake gitlab:import_export:data RAILS_ENV=production
+```
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../development/shared_files.md
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index 3411e4af6a7..4172b604cec 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -5,7 +5,7 @@ This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by
default because it still causes too many false alarms.
Git has a built-in mechanism, [git fsck][git-fsck], to verify the
-integrity of all data commited to a repository. GitLab administrators
+integrity of all data committed to a repository. GitLab administrators
can trigger such a check for a project via the project page under the
admin panel. The checks run asynchronously so it may take a few minutes
before the check result is visible on the project admin page. If the
@@ -41,4 +41,4 @@ alarms you can choose to clear ALL repository check states from the
---
[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck"
-[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" \ No newline at end of file
+[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation"
diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md
new file mode 100644
index 00000000000..81bfe173151
--- /dev/null
+++ b/doc/administration/repository_storages.md
@@ -0,0 +1,18 @@
+# Repository storages
+
+GitLab allows you to define repository storage paths to enable distribution of
+storage load between several mount points.
+
+## For installations from source
+
+Add your repository storage paths in your `gitlab.yml` under repositories -> storages, using key -> value pairs.
+
+>**Notes:**
+- You must have at least one storage path called `default`.
+- In order for backups to work correctly the storage path must **not** be a
+mount point and the GitLab user should have correct permissions for the parent
+directory of the path.
+
+## For omnibus installations
+
+Follow the instructions at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/configuration.md#storing-git-data-in-an-alternative-directory
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
new file mode 100644
index 00000000000..d127d7b85e5
--- /dev/null
+++ b/doc/administration/troubleshooting/debug.md
@@ -0,0 +1,169 @@
+# Debugging Tips
+
+Sometimes things don't work the way they should. Here are some tips on debugging issues out
+in production.
+
+## Mail not working
+
+A common problem is that mails are not being sent for some reason. Suppose you configured
+an SMTP server, but you're not seeing mail delivered. Here's how to check the settings:
+
+1. Run a Rails console:
+
+ ```sh
+ sudo gitlab-rails console production
+ ```
+
+ or for source installs:
+
+ ```sh
+ bundle exec rails console production
+ ```
+
+2. Look at the ActionMailer `delivery_method` to make sure it matches what you
+ intended. If you configured SMTP, it should say `:smtp`. If you're using
+ Sendmail, it should say `:sendmail`:
+
+ ```ruby
+ irb(main):001:0> ActionMailer::Base.delivery_method
+ => :smtp
+ ```
+
+3. If you're using SMTP, check the mail settings:
+
+ ```ruby
+ irb(main):002:0> ActionMailer::Base.smtp_settings
+ => {:address=>"localhost", :port=>25, :domain=>"localhost.localdomain", :user_name=>nil, :password=>nil, :authentication=>nil, :enable_starttls_auto=>true}```
+ ```
+
+ In the example above, the SMTP server is configured for the local machine. If this is intended, you may need to check your local mail
+ logs (e.g. `/var/log/mail.log`) for more details.
+
+4. Send a test message via the console.
+
+ ```ruby
+ irb(main):003:0> Notify.test_email('youremail@email.com', 'Hello World', 'This is a test message').deliver_now
+ ```
+
+ If you do not receive an e-mail and/or see an error message, then check
+ your mail server settings.
+
+## Advanced Issues
+
+For more advanced issues, `gdb` is a must-have tool for debugging issues.
+
+### The GNU Project Debugger (gdb)
+
+To install on Ubuntu/Debian:
+
+```
+sudo apt-get install gdb
+```
+
+On CentOS:
+
+```
+sudo yum install gdb
+```
+
+## Common Problems
+
+Many of the tips to diagnose issues below apply to many different situations. We'll use one
+concrete example to illustrate what you can do to learn what is going wrong.
+
+### 502 Gateway Timeout after unicorn spins at 100% CPU
+
+This error occurs when the Web server times out (default: 60 s) after not
+hearing back from the unicorn worker. If the CPU spins to 100% while this in
+progress, there may be something taking longer than it should.
+
+To fix this issue, we first need to figure out what is happening. The
+following tips are only recommended if you do NOT mind users being affected by
+downtime. Otherwise skip to the next section.
+
+1. Load the problematic URL
+1. Run `sudo gdb -p <PID>` to attach to the unicorn process.
+1. In the gdb window, type:
+
+ ```
+ call (void) rb_backtrace()
+ ```
+
+1. This forces the process to generate a Ruby backtrace. Check
+ `/var/log/gitlab/unicorn/unicorn_stderr.log` for the backtace. For example, you may see:
+
+ ```ruby
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:33:in `block in start'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:33:in `loop'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:36:in `block (2 levels) in start'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:44:in `sample'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `sample_objects'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `each_with_object'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:68:in `each'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:69:in `block in sample_objects'
+ from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/sampler.rb:69:in `name'
+ ```
+
+1. To see the current threads, run:
+
+ ```
+ apply all thread bt
+ ```
+
+1. Once you're done debugging with `gdb`, be sure to detach from the process and exit:
+
+ ```
+ detach
+ exit
+ ```
+
+Note that if the unicorn process terminates before you are able to run these
+commands, gdb will report an error. To buy more time, you can always raise the
+Unicorn timeout. For omnibus users, you can edit `/etc/gitlab/gitlab.rb` and
+increase it from 60 seconds to 300:
+
+```ruby
+unicorn['worker_timeout'] = 300
+```
+
+For source installations, edit `config/unicorn.rb`.
+
+[Reconfigure] GitLab for the changes to take effect.
+
+[Reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+
+#### Troubleshooting without affecting other users
+
+The previous section attached to a running unicorn process, and this may have
+undesirable effects for users trying to access GitLab during this time. If you
+are concerned about affecting others during a production system, you can run a
+separate Rails process to debug the issue:
+
+1. Log in to your GitLab account.
+1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC).
+1. Obtain the private token for your user (Profile Settings -> Account).
+1. Bring up the GitLab Rails console. For omnibus users, run:
+
+ ````
+ sudo gitlab-rails console
+ ```
+
+1. At the Rails console, run:
+
+ ```ruby
+ [1] pry(main)> app.get '<URL FROM STEP 1>/private_token?<TOKEN FROM STEP 2>'
+ ```
+
+ For example:
+
+ ```ruby
+ [1] pry(main)> app.get 'https://gitlab.com/gitlab-org/gitlab-ce/issues/1?private_token=123456'
+ ```
+
+1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID.
+1. Follow step 2 from the previous section on using gdb.
+
+# More information
+
+* [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/)
+* [Cheatsheet of using gdb and ruby processes](gdb-stuck-ruby.txt)
diff --git a/doc/administration/troubleshooting/gdb-stuck-ruby.txt b/doc/administration/troubleshooting/gdb-stuck-ruby.txt
new file mode 100644
index 00000000000..13d5dfcffa4
--- /dev/null
+++ b/doc/administration/troubleshooting/gdb-stuck-ruby.txt
@@ -0,0 +1,142 @@
+# Here's the script I'll use to demonstrate - it just loops forever:
+
+$ cat test.rb
+#!/usr/bin/env ruby
+
+loop do
+ sleep 1
+end
+
+# Now, I'll start the script in the background, and redirect stdout and stderr
+# to /dev/null:
+
+$ ruby ./test.rb >/dev/null 2>/dev/null &
+[1] 1343
+
+# Next, I'll grab the PID of the script (1343):
+
+$ ps aux | grep test.rb
+vagrant 1343 0.0 0.4 3884 1652 pts/0 S 14:42 0:00 ruby ./test.rb
+vagrant 1345 0.0 0.2 4624 852 pts/0 S+ 14:42 0:00 grep --color=auto test.rb
+
+# Now I start gdb. Note that I'm using sudo here. This may or may not be
+# necessary in your setup. I'd try without sudo first, and fall back to adding
+# it if the next step fails:
+
+$ sudo gdb
+GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
+Copyright (C) 2012 Free Software Foundation, Inc.
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law. Type "show copying"
+and "show warranty" for details.
+This GDB was configured as "i686-linux-gnu".
+For bug reporting instructions, please see:
+<http://bugs.launchpad.net/gdb-linaro/>.
+
+# OK, now I'm in gdb, and I want to instruct it to attach to our Ruby process.
+# I can do that using the 'attach' command, which takes a PID (the one we
+# gathered above):
+
+(gdb) attach 1343
+Attaching to process 1343
+Reading symbols from /opt/vagrant_ruby/bin/ruby...done.
+Reading symbols from /lib/i386-linux-gnu/librt.so.1...(no debugging symbols found)...done.
+Loaded symbols for /lib/i386-linux-gnu/librt.so.1
+Reading symbols from /lib/i386-linux-gnu/libdl.so.2...(no debugging symbols found)...done.
+Loaded symbols for /lib/i386-linux-gnu/libdl.so.2
+Reading symbols from /lib/i386-linux-gnu/libcrypt.so.1...(no debugging symbols found)...done.
+Loaded symbols for /lib/i386-linux-gnu/libcrypt.so.1
+Reading symbols from /lib/i386-linux-gnu/libm.so.6...(no debugging symbols found)...done.
+Loaded symbols for /lib/i386-linux-gnu/libm.so.6
+Reading symbols from /lib/i386-linux-gnu/libc.so.6...(no debugging symbols found)...done.
+Loaded symbols for /lib/i386-linux-gnu/libc.so.6
+Reading symbols from /lib/i386-linux-gnu/libpthread.so.0...(no debugging symbols found)...done.
+[Thread debugging using libthread_db enabled]
+Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
+Loaded symbols for /lib/i386-linux-gnu/libpthread.so.0
+Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
+Loaded symbols for /lib/ld-linux.so.2
+0xb770c424 in __kernel_vsyscall ()
+
+# Great, now gdb is attached to the target process. If the step above fails, try
+# going back and running gdb under sudo. The next thing I want to do is gather
+# C-level backtraces from all threads in the process. The following command
+# stands for 'thread apply all backtrace':
+
+(gdb) t a a bt
+
+Thread 1 (Thread 0xb74d76c0 (LWP 1343)):
+#0 0xb770c424 in __kernel_vsyscall ()
+#1 0xb75d7abd in select () from /lib/i386-linux-gnu/libc.so.6
+#2 0x08069c56 in rb_thread_wait_for (time=...) at eval.c:11376
+#3 0x080a20fd in rb_f_sleep (argc=1, argv=0xbf85f490) at process.c:1633
+#4 0x0805e0e2 in call_cfunc (argv=0xbf85f490, argc=1, len=-1, recv=3075299660, func=0x80a20b0 <rb_f_sleep>)
+ at eval.c:5778
+#5 rb_call0 (klass=3075304600, recv=3075299660, id=9393, oid=9393, argc=1, argv=0xbf85f490, body=0xb74c85a8, flags=2)
+ at eval.c:5928
+#6 0x0805e35d in rb_call (klass=3075304600, recv=3075299660, mid=9393, argc=1, argv=0xbf85f490, scope=1,
+ self=<optimized out>) at eval.c:6176
+#7 0x080651ec in rb_eval (self=3075299660, n=0xb74c4e1c) at eval.c:3521
+#8 0x0805c31c in rb_yield_0 (val=6, self=3075299660, klass=<optimized out>, flags=0, avalue=0) at eval.c:5095
+#9 0x0806a1e5 in loop_i () at eval.c:5227
+#10 0x08058dbd in rb_rescue2 (b_proc=0x806a1c0 <loop_i>, data1=0, r_proc=0, data2=0) at eval.c:5491
+#11 0x08058f28 in rb_f_loop () at eval.c:5252
+#12 0x0805e0c1 in call_cfunc (argv=0x0, argc=0, len=0, recv=3075299660, func=0x8058ef0 <rb_f_loop>) at eval.c:5781
+#13 rb_call0 (klass=3075304600, recv=3075299660, id=4121, oid=4121, argc=0, argv=0x0, body=0xb74d4dbc, flags=2)
+ at eval.c:5928
+#14 0x0805e35d in rb_call (klass=3075304600, recv=3075299660, mid=4121, argc=0, argv=0x0, scope=1, self=<optimized out>)
+ at eval.c:6176
+#15 0x080651ec in rb_eval (self=3075299660, n=0xb74c4dcc) at eval.c:3521
+#16 0x080662c6 in rb_eval (self=3075299660, n=0xb74c4de0) at eval.c:3236
+#17 0x08068ee4 in ruby_exec_internal () at eval.c:1654
+#18 0x08068f24 in ruby_exec () at eval.c:1674
+#19 0x0806b2cd in ruby_run () at eval.c:1684
+#20 0x08053771 in main (argc=2, argv=0xbf860204, envp=0xbf860210) at main.c:48
+
+# C backtraces are sometimes sufficient, but often Ruby backtraces are necessary
+# for debugging as well. Ruby has a built-in function called rb_backtrace() that
+# we can use to dump out a Ruby backtrace, but it prints to stdout or stderr
+# (depending on your Ruby version), which might have been redirected to a file
+# or to /dev/null (as in our example) when the process started up.
+#
+# To get aroundt this, we'll do a little trick and redirect the target process's
+# stdout and stderr to the current TTY, so that any output from the process
+# will appear directly on our screen.
+
+# First, let's close the existing file descriptors for stdout and stderr
+# (FD 1 and 2, respectively):
+(gdb) call (void) close(1)
+(gdb) call (void) close(2)
+
+# Next, we need to figure out the device name for the current TTY:
+(gdb) shell tty
+/dev/pts/0
+
+# OK, now we can pass the device name obtained above to open() and attach
+# file descriptors 1 and 2 back to the current TTY with these calls:
+
+(gdb) call (int) open("/dev/pts/0", 2, 0)
+$1 = 1
+(gdb) call (int) open("/dev/pts/0", 2, 0)
+$2 = 2
+
+# Finally, we call rb_backtrace() in order to dump the Ruby backtrace:
+
+(gdb) call (void) rb_backtrace()
+ from ./test.rb:4:in `sleep'
+ from ./test.rb:4
+ from ./test.rb:3:in `loop'
+ from ./test.rb:3
+
+# And here's how we get out of gdb. Once you've quit, you'll probably want to
+# clean up the stuck process by killing it.
+
+(gdb) quit
+A debugging session is active.
+
+ Inferior 1 [process 1343] will be detached.
+
+Quit anyway? (y or n) y
+Detaching from program: /opt/vagrant_ruby/bin/ruby, process 1343
+$
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 134a7583762..b71f8fabbc8 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -147,7 +147,16 @@ bt
To output a backtrace from all threads at once:
```
-apply all thread bt
+set pagination off
+thread apply all bt
+```
+
+Once you're done debugging with `gdb`, be sure to detach from the process and
+exit:
+
+```
+detach
+exit
```
## Check for blocking queries
diff --git a/doc/api/README.md b/doc/api/README.md
index ff039f1886f..d1e6c54c521 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -8,42 +8,50 @@ under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api).
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
-- [Project Snippets](project_snippets.md)
-- [Services](services.md)
-- [Repositories](repositories.md)
-- [Repository Files](repository_files.md)
-- [Commits](commits.md)
-- [Tags](tags.md)
+- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
-- [Merge Requests](merge_requests.md)
+- [Builds](builds.md)
+- [Build triggers](build_triggers.md)
+- [Build Variables](build_variables.md)
+- [Commits](commits.md)
+- [Deploy Keys](deploy_keys.md)
+- [Groups](groups.md)
- [Issues](issues.md)
+- [Keys](keys.md)
- [Labels](labels.md)
+- [Merge Requests](merge_requests.md)
- [Milestones](milestones.md)
-- [Notes](notes.md) (comments)
-- [Deploy Keys](deploy_keys.md)
-- [System Hooks](system_hooks.md)
-- [Groups](groups.md)
+- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
-- [Settings](settings.md)
-- [Keys](keys.md)
-- [Builds](builds.md)
-- [Build triggers](build_triggers.md)
-- [Build Variables](build_variables.md)
+- [Notes](notes.md) (comments)
+- [Projects](projects.md) including setting Webhooks
+- [Project Snippets](project_snippets.md)
+- [Repositories](repositories.md)
+- [Repository Files](repository_files.md)
- [Runners](runners.md)
-- [Licenses](licenses.md)
+- [Services](services.md)
+- [Session](session.md)
+- [Settings](settings.md)
+- [Sidekiq metrics](sidekiq_metrics.md)
+- [System Hooks](system_hooks.md)
+- [Tags](tags.md)
+- [Users](users.md)
+- [Todos](todos.md)
+
+### Internal CI API
+
+The following documentation is for the [internal CI API](ci/README.md):
+
+- [Builds](ci/builds.md)
+- [Runners](ci/runners.md)
## Authentication
-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 via a token. There are three types of tokens
+available: private tokens, OAuth 2 tokens, and personal access tokens.
-If `private_token` is invalid or omitted, then an error message will be
-returned with status code `401`:
+If a token is invalid or omitted, an error message will be returned with
+status code `401`:
```json
{
@@ -51,42 +59,56 @@ returned with status code `401`:
}
```
-API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url].
+### Private Tokens
-Example of a valid API request:
+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`).
-```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
-```
+### OAuth 2 Tokens
-Example of a valid API request using cURL and authentication via header:
+You can use an OAuth 2 token to authenticate with the API by passing it either in the
+`access_token` parameter or in the `Authorization` header.
+
+Example of using the OAuth2 token in the header:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl -H "Authorization: Bearer OAUTH-TOKEN" 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 an API URL.
+Read more about [GitLab as an OAuth2 client](oauth2.md).
+
+### Personal Access Tokens
-## Authentication with OAuth2 token
+> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
-Instead of the `private_token` you can transmit the OAuth2 access token as a
-header or as a parameter.
+You can create as many personal access tokens as you like from your GitLab
+profile (`/profile/personal_access_tokens`); perhaps one for each application
+that needs access to the GitLab API.
+
+Once you have your token, pass it to the API using either the `private_token`
+parameter or the `PRIVATE-TOKEN` header.
+
+## Basic Usage
+
+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 OAuth2 token as a parameter:
+Example of a valid API request:
```shell
-curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
```
-Example of OAuth2 token as a header:
+Example of a valid API request using cURL and authentication via header:
```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
## Status codes
@@ -323,3 +345,4 @@ 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
+[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
new file mode 100644
index 00000000000..b44f8cfd628
--- /dev/null
+++ b/doc/api/award_emoji.md
@@ -0,0 +1,367 @@
+# Award Emoji
+
+ >**Note:** This feature was introduced in GitLab 8.9
+
+An awarded emoji tells a thousand words, and can be awarded on issues, merge
+requests and notes/comments. Issues, merge requests and notes are further called
+`awardables`.
+
+## Issues and merge requests
+
+### List an awardable's award emoji
+
+Gets a list of all award emoji
+
+```
+GET /projects/:id/issues/:issue_id/award_emoji
+GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 4,
+ "name": "1234",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-15T10:09:34.206Z",
+ "updated_at": "2016-06-15T10:09:34.206Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+ },
+ {
+ "id": 1,
+ "name": "microphone",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.177Z",
+ "updated_at": "2016-06-15T10:09:34.177Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+ }
+]
+```
+
+### Get single issue note
+
+Gets a single award emoji
+
+```
+GET /projects/:id/issues/:issue_id/award_emoji/:award_id
+GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+| `award_id` | integer | yes | The ID of the award emoji |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+```
+
+Example Response:
+
+```json
+{
+ "id": 1,
+ "name": "microphone",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.177Z",
+ "updated_at": "2016-06-15T10:09:34.177Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+### Award a new emoji
+
+This end point creates an award emoji on the specified resource
+
+```
+POST /projects/:id/issues/:issue_id/award_emoji
+POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+| `name` | string | yes | The name of the emoji, without colons |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+```
+
+Example Response:
+
+```json
+{
+ "id": 344,
+ "name": "blowfish",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T17:47:29.266Z",
+ "updated_at": "2016-06-17T17:47:29.266Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+### Delete an award emoji
+
+Sometimes its just not meant to be, and you'll have to remove your award. Only available to
+admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+
+```
+DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
+DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `award_id` | integer | yes | The ID of a award_emoji |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
+```
+
+Example Response:
+
+```json
+{
+ "id": 344,
+ "name": "blowfish",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T17:47:29.266Z",
+ "updated_at": "2016-06-17T17:47:29.266Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+## Award Emoji on Notes
+
+The endpoints documented above are available for Notes as well. Notes
+are a sub-resource of Issues and Merge Requests. The examples below
+describe working with Award Emoji on notes for an Issue, but can be
+easily adapted for notes on a Merge Request.
+
+### List a note's award emoji
+
+```
+GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of an note |
+
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 2,
+ "name": "mood_bubble_lightning",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.197Z",
+ "updated_at": "2016-06-15T10:09:34.197Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+ }
+]
+```
+
+### Get single note's award emoji
+
+```
+GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of the award emoji |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+```
+
+Example Response:
+
+```json
+{
+ "id": 2,
+ "name": "mood_bubble_lightning",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.197Z",
+ "updated_at": "2016-06-15T10:09:34.197Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
+
+### Award a new emoji on a note
+
+```
+POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `name` | string | yes | The name of the emoji, without colons |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+```
+
+Example Response:
+
+```json
+{
+ "id": 345,
+ "name": "rocket",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T19:59:55.888Z",
+ "updated_at": "2016-06-17T19:59:55.888Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
+
+### Delete an award emoji
+
+Sometimes its just not meant to be, and you'll have to remove your award. Only available to
+admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+
+```
+DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of a award_emoji |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
+```
+
+Example Response:
+
+```json
+{
+ "id": 345,
+ "name": "rocket",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T19:59:55.888Z",
+ "updated_at": "2016-06-17T19:59:55.888Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 4c0a47d1ea0..2adea11247e 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -21,85 +21,85 @@ 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.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
},
- {
- "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": ""
- }
+ "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": ""
}
+ }
]
```
@@ -107,6 +107,11 @@ Example of response
Get a list of builds for specific commit in a project.
+This endpoint will return all builds, from all pipelines for a given commit.
+If the commit SHA is not found, it will respond with 404, otherwise it will
+return an array of builds (an empty array if there are no builds for this
+particular commit).
+
```
GET /projects/:id/repository/commits/:sha/builds
```
@@ -125,68 +130,68 @@ 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."
},
- {
- "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": ""
- }
+ "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": ""
}
+ }
]
```
@@ -211,42 +216,42 @@ 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": ""
- }
+ "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": ""
+ }
}
```
@@ -278,6 +283,30 @@ Response:
[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
+## Get a trace file
+
+Get a trace of a specific build of a project
+
+```
+GET /projects/:id/builds/:build_id/trace
+```
+
+| 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/trace"
+```
+
+Response:
+
+| Status | Description |
+|-----------|-----------------------------------|
+| 200 | Serves the trace file |
+| 404 | Build not found or no trace file |
+
## Cancel a build
Cancel a single build of a project
@@ -299,28 +328,28 @@ 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": "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
}
```
@@ -345,28 +374,28 @@ 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
+ "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
}
```
@@ -395,27 +424,77 @@ 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
+ "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
+}
+```
+
+## Keep artifacts
+
+Prevents artifacts from being deleted when expiration is set.
+
+```
+POST /projects/:id/builds/:build_id/artifacts/keep
+```
+
+Parameters
+
+| Attribute | Type | required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+Example request:
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
+```
+
+Example 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/ci/README.md b/doc/api/ci/README.md
new file mode 100644
index 00000000000..96a281e27c8
--- /dev/null
+++ b/doc/api/ci/README.md
@@ -0,0 +1,24 @@
+# GitLab CI API
+
+## Purpose
+
+The main purpose of GitLab CI API is to provide the necessary data and context
+for GitLab CI Runners.
+
+All relevant information about the consumer API can be found in a
+[separate document](../../api/README.md).
+
+## API Prefix
+
+The current CI API prefix is `/ci/api/v1`.
+
+You need to prepend this prefix to all examples in this documentation, like:
+
+```bash
+GET /ci/api/v1/builds/:id/artifacts
+```
+
+## Resources
+
+- [Builds](builds.md)
+- [Runners](runners.md)
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
new file mode 100644
index 00000000000..d779463fd8c
--- /dev/null
+++ b/doc/api/ci/builds.md
@@ -0,0 +1,138 @@
+# Builds API
+
+API used by runners to receive and update builds.
+
+>**Note:**
+This API is intended to be used only by Runners as their own
+communication channel. For the consumer API see the
+[Builds API](../builds.md).
+
+## Authentication
+
+This API uses two types of authentication:
+
+1. Unique Runner's token which is the token assigned to the Runner after it
+ has been registered.
+
+2. Using the build authorization token.
+ This is project's CI token that can be found under the **Builds** section of
+ a project's settings. The build authorization token can be passed as a
+ parameter or a value of `BUILD-TOKEN` header.
+
+These two methods of authentication are interchangeable.
+
+## Builds
+
+### Runs oldest pending build by runner
+
+```
+POST /ci/api/v1/builds/register
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `token` | string | yes | Unique runner token |
+
+
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+```
+
+### Update details of an existing build
+
+```
+PUT /ci/api/v1/builds/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | Unique runner token |
+| `state` | string | no | The state of a build |
+| `trace` | string | no | The trace of a build |
+
+```
+curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+```
+
+### Incremental build trace update
+
+Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
+with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
+must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
+Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
+
+For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
+header and a trace part covered by this range.
+
+For a valid update API will return `202` response with:
+* `Build-Status: {status}` header containing current status of the build,
+* `Range: 0-{length}` header with the current trace length.
+
+```
+PATCH /ci/api/v1/builds/:id/trace.txt
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a build |
+
+Headers:
+
+| Attribute | Type | Required | Description |
+|-----------------|---------|----------|-----------------------------------|
+| `BUILD-TOKEN` | string | yes | The build authorization token |
+| `Content-Range` | string | yes | Bytes range of trace that is sent |
+
+```
+curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+```
+
+
+### Upload artifacts to build
+
+```
+POST /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+| `file` | mixed | yes | Artifacts file |
+
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+```
+
+### Download the artifacts file from build
+
+```
+GET /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+
+```
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
+
+### Remove the artifacts file from build
+
+```
+DELETE /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| ` id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+
+```
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
new file mode 100644
index 00000000000..96b3c42f773
--- /dev/null
+++ b/doc/api/ci/runners.md
@@ -0,0 +1,57 @@
+# Runners API
+
+API used by Runners to register and delete themselves.
+
+>**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](../runners.md).
+
+## Authentication
+
+This API uses two types of authentication:
+
+1. Unique Runner's token, which is the token assigned to the Runner after it
+ has been registered.
+
+2. Using Runners' registration token.
+ This is a token that can be found in project's settings.
+ It can also be found in the **Admin > Runners** settings area.
+ There are two types of tokens you can pass: shared Runner registration
+ token or project specific registration token.
+
+## Register a new runner
+
+Used to make GitLab CI aware of available runners.
+
+```sh
+POST /ci/api/v1/runners/register
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | --------- | ----------- |
+| `token` | string | yes | Runner's registration token |
+
+Example request:
+
+```sh
+curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n"
+```
+
+## Delete a Runner
+
+Used to remove a Runner.
+
+```sh
+DELETE /ci/api/v1/runners/delete
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | --------- | ----------- |
+| `token` | string | yes | Runner's registration token |
+
+Example request:
+
+```sh
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n"
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 2821bc21b81..87480bebfc4 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -42,46 +42,49 @@ Parameters:
```json
[
{
- "id": 4,
- "description": null,
+ "id": 9,
+ "description": "foo",
"default_branch": "master",
+ "tag_list": [],
"public": false,
- "visibility_level": 0,
- "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
- "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
- "web_url": "http://example.com/diaspora/diaspora-client",
- "tag_list": [
- "example",
- "disapora client"
- ],
- "owner": {
- "id": 3,
- "name": "Diaspora",
- "created_at": "2013-09-30T13: 46: 02Z"
- },
- "name": "Diaspora Client",
- "name_with_namespace": "Diaspora / Diaspora Client",
- "path": "diaspora-client",
- "path_with_namespace": "diaspora/diaspora-client",
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+ "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "Experimental / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
"issues_enabled": true,
"merge_requests_enabled": true,
- "builds_enabled": true,
"wiki_enabled": true,
- "snippets_enabled": false,
- "created_at": "2013-09-30T13: 46: 02Z",
- "last_activity_at": "2013-09-30T13: 46: 02Z",
- "creator_id": 3,
+ "builds_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2016-04-05T21:40:50.169Z",
+ "last_activity_at": "2016-04-06T16:52:08.432Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
"namespace": {
- "created_at": "2013-09-30T13: 46: 02Z",
- "description": "",
- "id": 3,
- "name": "Diaspora",
- "owner_id": 1,
- "path": "diaspora",
- "updated_at": "2013-09-30T13: 46: 02Z"
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-04-05T21:40:49.152Z",
+ "updated_at": "2016-04-07T08:07:48.466Z",
+ "description": "foo",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 10
},
- "archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
+ "avatar_url": null,
+ "star_count": 1,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
}
]
```
@@ -96,7 +99,180 @@ GET /groups/:id
Parameters:
-- `id` (required) - The ID or path of a group
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "visibility_level": 20,
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/groups/twitter",
+ "projects": [
+ {
+ "id": 7,
+ "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": true,
+ "archived": false,
+ "visibility_level": 20,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
+ "web_url": "https://gitlab.example.com/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,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:25.578Z",
+ "last_activity_at": "2016-06-17T07:47:25.881Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
+ "web_url": "https://gitlab.example.com/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,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:24.661Z",
+ "last_activity_at": "2016-06-17T07:47:24.838Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 8,
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+ ],
+ "shared_projects": [
+ {
+ "id": 8,
+ "description": "Velit eveniet provident fugiat saepe eligendi autem.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
+ "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "https://gitlab.example.com/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,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:27.089Z",
+ "last_activity_at": "2016-06-17T07:47:27.310Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "H5bp",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:26.621Z",
+ "updated_at": "2016-06-17T07:47:26.621Z",
+ "description": "Id consequatur rem vel qui doloremque saepe.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 4,
+ "public_builds": true,
+ "shared_with_groups": [
+ {
+ "group_id": 4,
+ "group_name": "Twitter",
+ "group_access_level": 30
+ },
+ {
+ "group_id": 3,
+ "group_name": "Gitlab Org",
+ "group_access_level": 10
+ }
+ ]
+ }
+ ]
+}
+```
## New group
@@ -201,7 +377,8 @@ Example response:
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true
+ "public_builds": true,
+ "shared_with_groups": []
}
]
}
@@ -265,7 +442,6 @@ GET /groups/:id/members
{
"id": 1,
"username": "raymond_smith",
- "email": "ray@smith.org",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
@@ -274,7 +450,6 @@ GET /groups/:id/members
{
"id": 2,
"username": "john_doe",
- "email": "joh@doe.org",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
diff --git a/doc/api/issues.md b/doc/api/issues.md
index fc7a7ae0c0c..3ced787b23e 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -28,7 +28,7 @@ GET /issues?labels=foo,bar&state=opened
| 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 |
+| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
| `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` |
@@ -83,6 +83,82 @@ Example response:
]
```
+## List group issues
+
+Get a list of a group's issues.
+
+```
+GET /groups/:id/issues
+GET /groups/:id/issues?state=opened
+GET /groups/:id/issues?state=closed
+GET /groups/:id/issues?labels=foo
+GET /groups/:id/issues?labels=foo,bar
+GET /groups/:id/issues?labels=foo,bar&state=opened
+GET /groups/:id/issues?milestone=1.0.0
+GET /groups/:id/issues?milestone=1.0.0&state=opened
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a group |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `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/groups/4/issues
+```
+
+Example response:
+
+```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",
+ "subscribed" : false,
+ "user_notes_count": 1
+ }
+]
+```
+
## List project issues
Get a list of a project's issues.
@@ -104,7 +180,7 @@ GET /projects/:id/issues?iid=42
| `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 |
+| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
| `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` |
@@ -365,6 +441,9 @@ target project is not found, error `404` is returned. If the target project
equals the source project or the user has insufficient permissions to move an
issue, error `400` together with an explaining error message is returned.
+If a given label and/or milestone with the same name also exists in the target
+project, it will then be assigned to the issue that is being moved.
+
```
POST /projects/:id/issues/:issue_id/move
```
@@ -515,12 +594,103 @@ Example response:
"id": 11,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "http://lgitlab.example.com/u/orville"
+ "web_url": "https://gitlab.example.com/u/orville"
},
"subscribed": false
}
```
+## Create a todo
+
+Manually creates a todo for the current user on an issue. If the request is
+successful, status code `200` together with the created todo is returned. If
+there already exists a todo for the user on that issue, status code `304` is
+returned.
+
+```
+POST /projects/:id/issues/:issue_id/todo
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+```
+
+Example response:
+
+```json
+{
+ "id": 112,
+ "project": {
+ "id": 5,
+ "name": "Gitlab Ci",
+ "name_with_namespace": "Gitlab Org / Gitlab Ci",
+ "path": "gitlab-ci",
+ "path_with_namespace": "gitlab-org/gitlab-ci"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "Issue",
+ "target": {
+ "id": 93,
+ "iid": 10,
+ "project_id": 5,
+ "title": "Vel voluptas atque dicta mollitia adipisci qui at.",
+ "description": "Tempora laboriosam sint magni sed voluptas similique.",
+ "state": "closed",
+ "created_at": "2016-06-17T07:47:39.486Z",
+ "updated_at": "2016-07-01T11:09:13.998Z",
+ "labels": [],
+ "milestone": {
+ "id": 26,
+ "iid": 1,
+ "project_id": 5,
+ "title": "v0.0",
+ "description": "Accusantium nostrum rerum quae quia quis nesciunt suscipit id.",
+ "state": "closed",
+ "created_at": "2016-06-17T07:47:33.832Z",
+ "updated_at": "2016-06-17T07:47:33.832Z",
+ "due_date": null
+ },
+ "assignee": {
+ "name": "Jarret O'Keefe",
+ "username": "francisca",
+ "id": 14,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/francisca"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "subscribed": true,
+ "user_notes_count": 7,
+ "upvotes": 0,
+ "downvotes": 0
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
+ "body": "Vel voluptas atque dicta mollitia adipisci qui at.",
+ "state": "pending",
+ "created_at": "2016-07-01T11:09:13.992Z"
+}
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 3730c07c5a7..a181c0f57a2 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -39,7 +39,7 @@ Example response:
{
"name" : "critical",
"color" : "#d9534f",
- "description": "Criticalissue. Need fix ASAP",
+ "description": "Critical issue. Need fix ASAP",
"open_issues_count": 1,
"closed_issues_count": 3,
"open_merge_requests_count": 1
@@ -165,3 +165,73 @@ Example response:
"description": "Documentation"
}
```
+
+## Subscribe to a label
+
+Subscribes the authenticated user to a label to receive notifications. If the
+operation is successful, status code `201` together with the updated label is
+returned. If the user is already subscribed to the label, the status code `304`
+is returned. If the project or label is not found, status code `404` is
+returned.
+
+```
+POST /projects/:id/labels/:label_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ----------------- | -------- | ------------------------------------ |
+| `id` | integer | yes | The ID of a project |
+| `label_id` | integer or string | yes | The ID or title of a project's label |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+```
+
+Example response:
+
+```json
+{
+ "name": "Docs",
+ "color": "#cc0033",
+ "description": "",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": true
+}
+```
+
+## Unsubscribe from a label
+
+Unsubscribes the authenticated user from a label to not receive notifications
+from it. If the operation is successful, status code `200` together with the
+updated label is returned. If the user is not subscribed to the label, the
+status code `304` is returned. If the project or label is not found, status code
+`404` is returned.
+
+```
+DELETE /projects/:id/labels/:label_id/subscription
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ----------------- | -------- | ------------------------------------ |
+| `id` | integer | yes | The ID of a project |
+| `label_id` | integer or string | yes | The ID or title of a project's label |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+```
+
+Example response:
+
+```json
+{
+ "name": "Docs",
+ "color": "#cc0033",
+ "description": "",
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false
+}
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 8217e30fe25..aee94b3fc36 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -413,11 +413,13 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c
Merge changes submitted with MR using this API.
-If merge success you get `200 OK`.
+If the merge succeeds you'll get a `200 OK`.
-If it has some conflicts and can not be merged - you get 405 and error message 'Branch cannot be merged'
+If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged'
-If merge request is already merged or closed - you get 405 and error message 'Method Not Allowed'
+If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed'
+
+If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch'
If you don't have permissions to accept this merge request - you'll get a 401
@@ -431,7 +433,8 @@ Parameters:
- `merge_request_id` (required) - ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
- `should_remove_source_branch` (optional) - if `true` removes the source branch
-- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds
+- `merge_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds
+- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail
```json
{
@@ -569,7 +572,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
```
-Example response:
+Example response when the GitLab issue tracker is used:
```json
[
@@ -615,6 +618,17 @@ Example response:
]
```
+Example response when an external issue tracker (e.g. JIRA) is used:
+
+```json
+[
+ {
+ "id" : "PROJECT-123",
+ "title" : "Title of this issue"
+ }
+]
+```
+
## Subscribe to a merge request
Subscribes the authenticated user to a merge request to receive notification. If
@@ -762,3 +776,101 @@ Example response:
"subscribed": false
}
```
+
+## Create a todo
+
+Manually creates a todo for the current user on a merge request. If the
+request is successful, status code `200` together with the created todo is
+returned. If there already exists a todo for the user on that merge request,
+status code `304` is returned.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/todo
+```
+
+| 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 -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+```
+
+Example response:
+
+```json
+{
+ "id": 113,
+ "project": {
+ "id": 3,
+ "name": "Gitlab Ci",
+ "name_with_namespace": "Gitlab Org / Gitlab Ci",
+ "path": "gitlab-ci",
+ "path_with_namespace": "gitlab-org/gitlab-ci"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 27,
+ "iid": 7,
+ "project_id": 3,
+ "title": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
+ "description": "Veniam sunt nihil modi earum cumque illum delectus. Nihil ad quis distinctio quia. Autem eligendi at quibusdam repellendus.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:48:04.330Z",
+ "updated_at": "2016-07-01T11:14:15.537Z",
+ "target_branch": "allow_regex_for_project_skip_ref",
+ "source_branch": "backup",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Jarret O'Keefe",
+ "username": "francisca",
+ "id": 14,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/francisca"
+ },
+ "assignee": {
+ "name": "Dr. Gabrielle Strosin",
+ "username": "barrett.krajcik",
+ "id": 4,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/barrett.krajcik"
+ },
+ "source_project_id": 3,
+ "target_project_id": 3,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 27,
+ "iid": 2,
+ "project_id": 3,
+ "title": "v1.0",
+ "description": "Quis ea accusantium animi hic fuga assumenda.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:33.840Z",
+ "updated_at": "2016-06-17T07:47:33.840Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "unchecked",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
+ "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
+ "state": "pending",
+ "created_at": "2016-07-01T11:14:15.530Z"
+}
+```
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index d416a826f79..31902e145f6 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -65,6 +65,13 @@ curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
## Resource Owner Password Credentials
+## Deprecation Notice
+
+1. Starting in GitLab 9.0, the Resource Owner Password Credentials will be *disabled* for users with two-factor authentication turned on.
+2. These users can access the API using [personal access tokens] instead.
+
+---
+
In this flow, a token is requested in exchange for the resource owner credentials (username and password).
The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
@@ -100,3 +107,5 @@ client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http
access_token = client.password.get_token('user@example.com', 'sekret')
puts access_token.token
```
+
+[personal access tokens]: ./README.md#personal-access-tokens
diff --git a/doc/api/projects.md b/doc/api/projects.md
index de1faadebf5..90f8c7f79bb 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -82,7 +82,8 @@ Parameters:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true
+ "public_builds": true,
+ "shared_with_groups": []
},
{
"id": 6,
@@ -140,7 +141,8 @@ Parameters:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true
+ "public_builds": true,
+ "shared_with_groups": []
}
]
```
@@ -262,7 +264,20 @@ Parameters:
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
- "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
+ "public_builds": true,
+ "shared_with_groups": [
+ {
+ "group_id": 4,
+ "group_name": "Twitter",
+ "group_access_level": 30
+ },
+ {
+ "group_id": 3,
+ "group_name": "Gitlab Org",
+ "group_access_level": 10
+ }
+ ]
}
```
@@ -424,6 +439,7 @@ Parameters:
- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
+- `container_registry_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `import_url` (optional)
@@ -447,6 +463,7 @@ Parameters:
- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
+- `container_registry_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `import_url` (optional)
@@ -472,6 +489,7 @@ Parameters:
- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
+- `container_registry_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `public_builds` (optional)
@@ -550,7 +568,9 @@ Example response:
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
- "star_count": 1
+ "star_count": 1,
+ "public_builds": true,
+ "shared_with_groups": []
}
```
@@ -613,7 +633,9 @@ Example response:
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
- "star_count": 0
+ "star_count": 0,
+ "public_builds": true,
+ "shared_with_groups": []
}
```
@@ -696,7 +718,9 @@ Example response:
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
- "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
+ "public_builds": true,
+ "shared_with_groups": []
}
```
@@ -710,7 +734,7 @@ have the proper access rights, code 403 is returned. Status 404 is returned if t
doesn't exist, or is hidden to the user.
```
-POST /projects/:id/archive
+POST /projects/:id/unarchive
```
| Attribute | Type | Required | Description |
@@ -779,7 +803,9 @@ Example response:
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
- "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
+ "public_builds": true,
+ "shared_with_groups": []
}
```
diff --git a/doc/api/runners.md b/doc/api/runners.md
index cc6c6b7cb2f..ddfa298f79d 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -275,7 +275,7 @@ POST /projects/:id/runners
| `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"
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9"
```
Example response:
@@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id
| `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"
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
```
Example response:
diff --git a/doc/api/services.md b/doc/api/services.md
index 7d45b2cf463..f821a614047 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -8,7 +8,7 @@ Asana - Teamwork without email
Set Asana service for a project.
-> This service adds commit messages as comments to Asana tasks. Once enabled, commit messages are checked for Asana task URLs (for example, `https://app.asana.com/0/123456/987654`) or task IDs starting with # (for example, `#987654`). Every task ID found will 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
+> This service adds commit messages as comments to Asana tasks. Once enabled, commit messages are checked for Asana task URLs (for example, `https://app.asana.com/0/123456/987654`) or task IDs starting with # (for example, `#987654`). Every task ID found will 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: https://asana.com/developers/documentation/getting-started/auth#api-key
```
PUT /projects/:id/services/asana
@@ -16,8 +16,8 @@ PUT /projects/:id/services/asana
Parameters:
-- `api_key` (**required**) - User API token. User must have access to task,all comments will be attributed to this user.
-- `restrict_to_branch` (optional) - Comma-separated list of branches which will beautomatically inspected. Leave blank to include all branches.
+- `api_key` (**required**) - User API token. User must have access to task, all comments will be attributed to this user.
+- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
### Delete Asana service
@@ -374,40 +374,6 @@ Get Gemnasium service settings for a project.
GET /projects/:id/services/gemnasium
```
-## GitLab CI
-
-Continuous integration server from GitLab
-
-### Create/Edit GitLab CI service
-
-Set GitLab CI service for a project.
-
-```
-PUT /projects/:id/services/gitlab-ci
-```
-
-Parameters:
-
-- `token` (**required**) - GitLab CI project specific token
-- `project_url` (**required**) - http://ci.gitlabhq.com/projects/3
-- `enable_ssl_verification` (optional) - Enable SSL verification
-
-### Delete GitLab CI service
-
-Delete GitLab CI service for a project.
-
-```
-DELETE /projects/:id/services/gitlab-ci
-```
-
-### Get GitLab CI service settings
-
-Get GitLab CI service settings for a project.
-
-```
-GET /projects/:id/services/gitlab-ci
-```
-
## HipChat
Private group chat and IM
@@ -491,7 +457,7 @@ Jira issue tracker
Set JIRA service for a project.
-> 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. 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)
+> 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://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html)
```
PUT /projects/:id/services/jira
@@ -503,6 +469,8 @@ Parameters:
- `project_url` (**required**) - Project url
- `issues_url` (**required**) - Issue url
- `description` (optional) - Jira issue tracker
+- `username` (optional) - Jira username
+- `password` (optional) - Jira password
### Delete JIRA service
diff --git a/doc/api/session.md b/doc/api/session.md
index 71e93d0bb0a..066a055702d 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -1,5 +1,12 @@
# Session
+## Deprecation Notice
+
+1. Starting in GitLab 9.0, this feature will be *disabled* for users with two-factor authentication turned on.
+2. These users can access the API using [personal access tokens] instead.
+
+---
+
You can login with both GitLab and LDAP credentials in order to obtain the
private token.
@@ -45,3 +52,5 @@ Example response:
"private_token": "9koXpg98eAheJpvBs5tK"
}
```
+
+[personal access tokens]: ./README.md#personal-access-tokens
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 1e745115dc8..d9b68eaeadf 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -37,7 +37,9 @@ Example response:
"created_at" : "2016-01-04T15:44:55.176Z",
"default_project_visibility" : 0,
"gravatar_enabled" : true,
- "sign_in_text" : null
+ "sign_in_text" : null,
+ "container_registry_token_expire_delay": 5,
+ "repository_storage": "default"
}
```
@@ -55,7 +57,7 @@ PUT /application/settings
| `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`. |
+| `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 `2`. |
| `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 |
@@ -64,6 +66,9 @@ PUT /application/settings
| `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 |
+| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
+| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
+| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -90,6 +95,8 @@ Example response:
"default_snippet_visibility": 0,
"restricted_signup_domains": [],
"user_oauth_applications": true,
- "after_sign_out_path": ""
+ "after_sign_out_path": "",
+ "container_registry_token_expire_delay": 5,
+ "repository_storage": "default"
}
```
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
new file mode 100644
index 00000000000..ebd131c94ca
--- /dev/null
+++ b/doc/api/sidekiq_metrics.md
@@ -0,0 +1,152 @@
+# Sidekiq Metrics
+
+>**Note:** This endpoint is only available on GitLab 8.9 and above.
+
+This API endpoint allows you to retrieve some information about the current state
+of Sidekiq, its jobs, queues, and processes.
+
+## Get the current Queue Metrics
+
+List information about all the registered queues, their backlog and their
+latency.
+
+```
+GET /sidekiq/queue_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+```
+
+Example response:
+
+```json
+{
+ "queues": {
+ "default": {
+ "backlog": 0,
+ "latency": 0
+ }
+ }
+}
+```
+
+## Get the current Process Metrics
+
+List information about all the Sidekiq workers registered to process your queues.
+
+```
+GET /sidekiq/process_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+```
+
+Example response:
+
+```json
+{
+ "processes": [
+ {
+ "hostname": "gitlab.example.com",
+ "pid": 5649,
+ "tag": "gitlab",
+ "started_at": "2016-06-14T10:45:07.159-05:00",
+ "queues": [
+ "post_receive",
+ "mailers",
+ "archive_repo",
+ "system_hook",
+ "project_web_hook",
+ "gitlab_shell",
+ "incoming_email",
+ "runner",
+ "common",
+ "default"
+ ],
+ "labels": [],
+ "concurrency": 25,
+ "busy": 0
+ }
+ ]
+}
+```
+
+## Get the current Job Statistics
+
+List information about the jobs that Sidekiq has performed.
+
+```
+GET /sidekiq/job_stats
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+```
+
+Example response:
+
+```json
+{
+ "jobs": {
+ "processed": 2,
+ "failed": 0,
+ "enqueued": 0
+ }
+}
+```
+
+## Get a compound response of all the previously mentioned metrics
+
+List all the currently available information about Sidekiq.
+
+```
+GET /sidekiq/compound_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+```
+
+Example response:
+
+```json
+{
+ "queues": {
+ "default": {
+ "backlog": 0,
+ "latency": 0
+ }
+ },
+ "processes": [
+ {
+ "hostname": "gitlab.example.com",
+ "pid": 5649,
+ "tag": "gitlab",
+ "started_at": "2016-06-14T10:45:07.159-05:00",
+ "queues": [
+ "post_receive",
+ "mailers",
+ "archive_repo",
+ "system_hook",
+ "project_web_hook",
+ "gitlab_shell",
+ "incoming_email",
+ "runner",
+ "common",
+ "default"
+ ],
+ "labels": [],
+ "concurrency": 25,
+ "busy": 0
+ }
+ ],
+ "jobs": {
+ "processed": 2,
+ "failed": 0,
+ "enqueued": 0
+ }
+}
+```
+
diff --git a/doc/api/todos.md b/doc/api/todos.md
new file mode 100644
index 00000000000..29e73664410
--- /dev/null
+++ b/doc/api/todos.md
@@ -0,0 +1,444 @@
+# Todos
+
+**Note:** This feature was [introduced][ce-3188] in GitLab 8.10
+
+## Get a list of todos
+
+Returns a list of todos. When no filter is applied, it returns all pending todos
+for the current user. Different filters allow the user to precise the request.
+
+```
+GET /todos
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, or `marked`. |
+| `author_id` | integer | no | The ID of an author |
+| `project_id` | integer | no | The ID of a project |
+| `state` | string | no | The state of the todo. Can be either `pending` or `done` |
+| `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "pending",
+ "created_at": "2016-06-17T07:52:35.225Z"
+ },
+ {
+ "id": 98,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "action_name": "assigned",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "pending",
+ "created_at": "2016-06-17T07:49:24.624Z"
+ }
+]
+```
+
+## Mark a todo as done
+
+Marks a single pending todo given by its ID for the current user as done. The
+todo marked as done is returned in the response.
+
+```
+DELETE /todos/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a todo |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+```
+
+Example Response:
+
+```json
+{
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:52:35.225Z"
+}
+```
+
+## Mark all todos as done
+
+Marks all pending todos for the current user as done. All todos marked as done
+are returned in the response.
+
+```
+DELETE /todos
+```
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:52:35.225Z"
+ },
+ {
+ "id": 98,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "action_name": "assigned",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:49:24.624Z"
+ },
+]
+```
+
+[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 4abc45bf9bb..a9d407528e8 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -5,6 +5,8 @@
- [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)
+- [Pipelines and builds](pipelines.md)
+- [Environments and deployments](environments.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)
@@ -14,5 +16,5 @@
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](build_artifacts/README.md)
- [User permissions](permissions/README.md)
-- [API](api/README.md)
+- [API](../api/ci/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 aea808007fc..4ca8d92d7cc 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -1,22 +1,3 @@
# GitLab CI API
-## Purpose
-
-Main purpose of GitLab CI API is to provide necessary data and context for
-GitLab CI Runners.
-
-For consumer API take a look at this [documentation](../../api/README.md) where
-you will find all relevant information.
-
-## API Prefix
-
-Current CI API prefix is `/ci/api/v1`.
-
-You need to prepend this prefix to all examples in this documentation, like:
-
- GET /ci/api/v1/builds/:id/artifacts
-
-## Resources
-
-- [Builds](builds.md)
-- [Runners](runners.md)
+This document was moved to a [new location](../../api/ci/README.md).
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index 79761a893da..f5bd3181c02 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -1,139 +1,3 @@
# Builds API
-API used by runners to receive and update builds.
-
-_**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/api/v1/builds/register
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `token` | string | yes | Unique runner token |
-
-
-```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
-```
-
-### Update details of an existing build
-
-```
-PUT /ci/api/v1/builds/:id
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a project |
-| `token` | string | yes | Unique runner token |
-| `state` | string | no | The state of a build |
-| `trace` | string | no | The trace of a build |
-
-```
-curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
-```
-
-### Incremental build trace update
-
-Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
-with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
-must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
-Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
-
-For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
-header and a trace part covered by this range.
-
-For a valid update API will return `202` response with:
-* `Build-Status: {status}` header containing current status of the build,
-* `Range: 0-{length}` header with the current trace length.
-
-```
-PATCH /ci/api/v1/builds/:id/trace.txt
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a build |
-
-Headers:
-
-| Attribute | Type | Required | Description |
-|-----------------|---------|----------|-----------------------------------|
-| `BUILD-TOKEN` | string | yes | The build authorization token |
-| `Content-Range` | string | yes | Bytes range of trace that is sent |
-
-```
-curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
-```
-
-
-### Upload artifacts to build
-
-```
-POST /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-| `file` | mixed | yes | Artifacts file |
-
-```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
-```
-
-### Download the artifacts file from build
-
-```
-GET /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
-```
-
-### Remove the artifacts file from build
-
-```
-DELETE /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| ` id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
-```
+This document was moved to a [new location](../../api/ci/builds.md).
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index 2f01da4bd76..b14ea99db76 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -1,46 +1,3 @@
# Runners API
-API used by runners to register and delete themselves.
-
-_**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
-
-This API uses two types of authentication:
-
-1. Unique runner's token
-
- Token assigned to runner after it has been registered.
-
-2. Using runners' registration token
-
- 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
-
-### Register a new runner
-
-Used to make GitLab CI aware of available runners.
-
- POST /ci/api/v1/runners/register
-
-Parameters:
-
- * `token` (required) - Registration token
-
-
-### Delete a runner
-
-Used to remove runner.
-
- DELETE /ci/api/v1/runners/delete
-
-Parameters:
-
- * `token` (required) - Unique runner token
+This document was moved to a [new location](../../api/ci/runners.md).
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser.png b/doc/ci/build_artifacts/img/build_artifacts_browser.png
index 73ed4eeb927..59cf2b8746b 100644
--- a/doc/ci/build_artifacts/img/build_artifacts_browser.png
+++ 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
index f5d15bc3e7d..7801c2e6fa6 100644
--- a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
+++ b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
Binary files differ
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index ca52a483a59..7f83f846454 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -4,14 +4,14 @@ GitLab CI allows you to use Docker Engine to build and test docker-based project
**This also allows to you to use `docker-compose` and other docker-enabled tools.**
-This is one of new trends in Continuous Integration/Deployment to:
+One of the new trends in Continuous Integration/Deployment is to:
-1. create application image,
-1. run test against created image,
-1. push image to remote registry,
-1. deploy server from pushed image
+1. create an application image,
+1. run tests against the created image,
+1. push image to a remote registry, and
+1. deploy to a server from the pushed image.
-It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image:
+It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
```bash
$ docker build -t my-image dockerfiles/
$ docker run my-docker-image /script/to/run/tests
@@ -19,24 +19,25 @@ $ docker tag my-image my-registry:5000/my-image
$ docker push my-registry:5000/my-image
```
-However, this requires special configuration of GitLab Runner to enable `docker` support during build.
-**This requires running GitLab Runner in privileged mode which can be harmful when untrusted code is run.**
+This requires special configuration of GitLab Runner to enable `docker` support during builds.
-There are two methods to enable the use of `docker build` and `docker run` during build.
+## Runner Configuration
-## 1. Use shell executor
+There are three methods to enable the use of `docker build` and `docker run` during builds; each with their own tradeoffs.
+
+### Use shell executor
The simplest approach is to install GitLab Runner in `shell` execution mode.
-GitLab Runner then executes build scripts as `gitlab-runner` user.
+GitLab Runner then executes build scripts as the `gitlab-runner` user.
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
```bash
- $ sudo gitlab-runner register -n \
+ $ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor shell
--description "My Runner"
```
@@ -70,16 +71,18 @@ GitLab Runner then executes build scripts as `gitlab-runner` user.
5. You can now use `docker` command and install `docker-compose` if needed.
-6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions.
-For more information please checkout [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
+> **Note:**
+* By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
+For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
-## 2. Use docker-in-docker executor
+### Use docker-in-docker executor
-The second approach is to use the special Docker image with all tools installed
+The second approach is to use the special docker-in-docker (dind)
+[Docker image](https://hub.docker.com/_/docker/) with all tools installed
(`docker` and `docker-compose`) and run the build script in context of that
image in privileged mode.
-In order to do that follow the steps:
+In order to do that, follow the steps:
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
@@ -87,9 +90,9 @@ In order to do that follow the steps:
mode:
```bash
- sudo gitlab-runner register -n \
+ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:latest" \
@@ -119,11 +122,7 @@ In order to do that follow the steps:
Insecure = false
```
- If you want to use the Shared Runners available on your GitLab CE/EE
- installation in order to build Docker images, then make sure that your
- Shared Runners configuration has the `privileged` mode set to `true`.
-
-1. You can now use `docker` from build script:
+1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
```yaml
image: docker:latest
@@ -141,14 +140,177 @@ In order to do that follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-1. However, by enabling `--docker-privileged` you are effectively disabling all
- the security mechanisms of containers and exposing your host to privilege
- escalation which can lead to container breakout.
-
- For more information, check out the official Docker documentation on
- [Runtime privilege and Linux capabilities][docker-cap].
+Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
+* By enabling `--docker-privileged`, you are effectively disabling all of
+the security mechanisms of containers and exposing your host to privilege
+escalation which can lead to container breakout. For more information, check out the official Docker documentation on
+[Runtime privilege and Linux capabilities][docker-cap].
+* Using docker-in-docker, each build is in a clean environment without the past
+history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
+* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
+offered.
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
+### Use Docker socket binding
+
+The third approach is to bind-mount `/var/run/docker.sock` into the container so that docker is available in the context of that image.
+
+In order to do that, follow the steps:
+
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+
+1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
+
+ ```bash
+ sudo gitlab-ci-multi-runner register -n \
+ --url https://gitlab.com/ci \
+ --registration-token REGISTRATION_TOKEN \
+ --executor docker \
+ --description "My Docker Runner" \
+ --docker-image "docker:latest" \
+ --docker-volumes /var/run/docker.sock:/var/run/docker.sock
+ ```
+
+ The above command will register a new Runner to use the special
+ `docker:latest` image which is provided by Docker. **Notice that it's using
+ the Docker daemon of the Runner itself, and any containers spawned by docker commands will be siblings of the Runner rather than children of the runner.** This may have complications and limitations that are unsuitable for your workflow.
+
+ The above command will create a `config.toml` entry similar to this:
+
+ ```
+ [[runners]]
+ url = "https://gitlab.com/ci"
+ token = REGISTRATION_TOKEN
+ executor = "docker"
+ [runners.docker]
+ tls_verify = false
+ image = "docker:latest"
+ privileged = false
+ disable_cache = false
+ volumes = ["/var/run/docker.sock", "/cache"]
+ [runners.cache]
+ Insecure = false
+ ```
+
+1. You can now use `docker` in the build script (note that you don't need to include the `docker:dind` service as when using the Docker in Docker executor):
+
+ ```yaml
+ image: docker:latest
+
+ before_script:
+ - docker info
+
+ build:
+ stage: build
+ script:
+ - docker build -t my-docker-image .
+ - docker run my-docker-image /script/to/run/tests
+ ```
+
+While the above method avoids using Docker in privileged mode, you should be aware of the following implications:
+* By sharing the docker daemon, you are effectively disabling all
+the security mechanisms of containers and exposing your host to privilege
+escalation which can lead to container breakout. For example, if a project
+ran `docker rm -f $(docker ps -a -q)` it would remove the GitLab Runner
+containers.
+* Concurrent builds may not work; if your tests
+create containers with specific names, they may conflict with each other.
+* Sharing files and directories from the source repo into containers may not
+work as expected since volume mounting is done in the context of the host
+machine, not the build container.
+e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
+
+## Using the GitLab Container Registry
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../container_registry/README.md). For example, if you're using
+docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
+
+
+```yaml
+ build:
+ image: docker:latest
+ services:
+ - docker:dind
+ stage: build
+ script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker build -t registry.example.com/group/project:latest .
+ - docker push registry.example.com/group/project:latest
+```
+
+You have to use the credentials of the special `gitlab-ci-token` user with its
+password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
+to your project. This allows you to automate building and deployment of your
+Docker images.
+
+Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
+including two tests that run in parallel. The build is stored in the container
+registry and used by subsequent stages, downloading the image
+when needed. Changes to `master` also get tagged as `latest` and deployed using
+an application-specific deploy script:
+
+```yaml
+image: docker:latest
+services:
+- docker:dind
+
+stages:
+- build
+- test
+- release
+- deploy
+
+variables:
+ CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME
+ CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+
+build:
+ stage: build
+ script:
+ - docker build --pull -t $CONTAINER_TEST_IMAGE .
+ - docker push $CONTAINER_TEST_IMAGE
+
+test1:
+ stage: test
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker run $CONTAINER_TEST_IMAGE /script/to/run/tests
+
+test2:
+ stage: test
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker run $CONTAINER_TEST_IMAGE /script/to/run/another/test
+
+release-image:
+ stage: release
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
+ - docker push $CONTAINER_RELEASE_IMAGE
+ only:
+ - master
+
+deploy:
+ stage: deploy
+ script:
+ - ./deploy.sh
+ only:
+ - master
+```
+
+Some things you should be aware of when using the Container Registry:
+* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job.
+* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images.
+* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed.
+* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously.
+
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 56ac2195c49..a849905ac6b 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -23,7 +23,7 @@ To use GitLab Runner with docker you need to register a new runner to use the
`docker` executor:
```bash
-gitlab-runner register \
+gitlab-ci-multi-runner register \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
new file mode 100644
index 00000000000..d85b8a34ced
--- /dev/null
+++ b/doc/ci/environments.md
@@ -0,0 +1,58 @@
+# Introduction to environments and deployments
+
+>**Note:**
+Introduced in GitLab 8.9.
+
+## Environments
+
+Environments are places where code gets deployed, such as staging or production.
+CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment.
+Defining environments in a project's `.gitlab-ci.yml` lets developers track
+[deployments] to these environments.
+
+## Deployments
+
+Deployments are created when [jobs] deploy versions of code to [environments].
+
+## Defining environments
+
+You can create and delete environments manually in the web interface, but we
+recommend that you define your environments in `.gitlab-ci.yml` first, which
+will automatically create environments for you after the first deploy.
+
+The `environment` is just a hint for GitLab that this job actually deploys to
+this environment. Each time the job succeeds, a deployment is recorded,
+remembering the git SHA and environment.
+
+Add something like this to your `.gitlab-ci.yml`:
+```
+production:
+ stage: deploy
+ script: dpl...
+ environment: production
+```
+
+See full [documentation](yaml/README.md#environment).
+
+## Seeing environment status
+
+You can find the environment list under **Pipelines > Environments** for your
+project. You'll see the git SHA and date of the last deployment to each
+environment defined.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+## Seeing deployment history
+
+Clicking on an environment will show the history of deployments.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+[Pipelines]: pipelines.md
+[jobs]: yaml/README.md#jobs
+[environments]: #environments
+[deployments]: #deployments
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 61294be599d..c134106bfd0 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -4,7 +4,8 @@
- [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)
+- [Test a Scala application](test-scala-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.
@@ -13,3 +14,4 @@
- [Blog 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)
+- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml)
diff --git a/doc/ci/deployment/README.md b/doc/ci/examples/deployment/README.md
index 7d91ce6710f..7d91ce6710f 100644
--- a/doc/ci/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 26953014502..17e1c64bb8a 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -263,10 +263,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-runner exec docker test:app
+gitlab-ci-multi-runner exec docker test:app
# Check using shell executor
-gitlab-runner exec shell test:app
+gitlab-ci-multi-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
new file mode 100644
index 00000000000..7412fdbbc78
--- /dev/null
+++ b/doc/ci/examples/test-scala-application.md
@@ -0,0 +1,47 @@
+## Test a Scala application
+
+This example demonstrates the integration of Gitlab CI with Scala
+applications using SBT. Checkout the example
+[project](https://gitlab.com/gitlab-examples/scala-sbt) and
+[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+``` yaml
+image: java:8
+
+before_script:
+ - apt-get update -y
+ - apt-get install apt-transport-https -y
+ # Install SBT
+ - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ - apt-get update -y
+ - apt-get install sbt -y
+ - sbt sbt-version
+
+test:
+ script:
+ - sbt clean coverage test coverageReport
+```
+
+The `before_script` installs [SBT](http://www.scala-sbt.org/) and
+displays the version that is being used. The `test` stage executes SBT
+to compile and test the project.
+[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
+plugin to measure test coverage.
+
+You can use other versions of Scala and SBT by defining them in
+`build.sbt`.
+
+### Display test coverage in build
+
+Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
+**Settings > Edit Project > Test coverage parsing** project setting to
+retrieve the test coverage rate from the build trace and have it
+displayed with your builds.
+
+**Builds** must be enabled for this option to appear.
diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png
index d088b8b329d..35780e277ae 100644
--- a/doc/ci/img/builds_tab.png
+++ b/doc/ci/img/builds_tab.png
Binary files differ
diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png
index 17aba5d14d8..38d7036f606 100644
--- a/doc/ci/img/features_settings.png
+++ b/doc/ci/img/features_settings.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
new file mode 100644
index 00000000000..48a9f994759
--- /dev/null
+++ b/doc/ci/pipelines.md
@@ -0,0 +1,38 @@
+# Introduction to pipelines and builds
+
+>**Note:**
+Introduced in GitLab 8.8.
+
+## Pipelines
+
+A pipeline is a group of [builds] that get executed in [stages] (batches). All
+of the builds in a stage are executed in parallel (if there are enough
+concurrent [runners]), and if they all succeed, the pipeline moves on to the
+next stage. If one of the builds fails, the next stage is not (usually)
+executed.
+
+## Builds
+
+Builds are individual runs of [jobs]. Not to be confused with a `build` job or
+`build` stage.
+
+## Defining pipelines
+
+Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in
+[stages].
+
+See full [documentation](yaml/README.md#jobs).
+
+## Seeing pipeline status
+
+You can find the current and historical pipeline runs under **Pipelines** for your
+project.
+
+## Seeing build status
+
+Clicking on a pipeline will show the builds that were run for that pipeline.
+
+[builds]: #builds
+[jobs]: yaml/README.md#jobs
+[stages]: yaml/README.md#stages
+[runners]: runners/README.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 6a42a935abd..7fa1a478f34 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -4,41 +4,41 @@
is fully integrated into GitLab itself and is [enabled] by default on all
projects.
-The TL;DR version of how GitLab CI works is the following.
-
----
-
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.
+push triggers your CI [pipeline].
-The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it
-runs three [stages]: `build`, `test`, and `deploy`.
+The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
+a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
+use all three stages; stages with no jobs are simply ignored.
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
+easy to see whether a merge request caused any of the tests to fail before
you even look at the code.
-Most projects only use GitLab's CI service to run the test suite so that
+Most projects use GitLab's CI service to run the test suite so that
developers get immediate feedback if they broke something.
+There's a growing trend to use continuous delivery and continuous deployment to
+automatically deploy tested code to staging and production environments.
+
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
-automagically started by the Runner and will appear under the project's
-`/builds` page.
+From there on, on every push to your Git repository, the Runner will
+automagically start the pipeline and the pipeline will appear under the
+project's `/pipelines` page.
---
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)
+ [GitLab.com](https://gitlab.com)
- 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.
@@ -57,15 +57,14 @@ On any push to your repository, GitLab will look for the `.gitlab-ci.yml`
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 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].
+Because `.gitlab-ci.yml` is in the repository and is version controlled, old
+versions still build successfully, forks can easily make use of CI, branches can
+have different pipelines and jobs, 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 indentation. Always use spaces, not
-tabs.
+so you have to pay extra attention to indentation. Always use spaces, not tabs.
### Creating a simple `.gitlab-ci.yml` file
@@ -108,7 +107,7 @@ If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
the link under **Settings > CI settings** in your project.
-For more information and a complete `.gitlab-ci.yml` syntax, please check
+For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
### Push `.gitlab-ci.yml` to GitLab
@@ -122,7 +121,8 @@ git commit -m "Add .gitlab-ci.yml"
git push origin master
```
-Now if you go to the **Builds** page you will see that the builds are pending.
+Now if you go to the **Pipelines** page you will see that the pipeline is
+pending.
You can also go to the **Commits** page and notice the little clock icon next
to the commit SHA.
@@ -138,15 +138,14 @@ Notice that there are two jobs pending which are named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
yet for these builds.
-The next step is to configure a Runner so that it picks the pending jobs.
+The next step is to configure a Runner so that it picks the pending builds.
## Configuring a Runner
-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 have Internet access.
+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 requirement is that the Runner's machine has 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_.
@@ -188,12 +187,16 @@ To enable **Shared Runners** you have to go to your project's
[Read more on Shared Runners](../runners/README.md).
-## Seeing the status of your build
+## Seeing the status of your pipeline and builds
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.
+You can view all pipelines by going to the **Pipelines** page in your project.
+
+![Commit status](img/pipelines_status.png)
+
+Or you can view all builds, by going to the **Pipelines > Builds** page.
![Commit status](img/builds_status.png)
@@ -212,8 +215,8 @@ 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).
+For more information read the
+[Builds emails service documentation](../../project_services/builds_emails.md).
## Builds badge
@@ -238,3 +241,4 @@ CI with various languages.
[runner]: ../runners/README.md
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
+[pipeline]: ../pipelines.md
diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png
index 89e6cd40cb6..b53a6cd86b0 100644
--- a/doc/ci/quick_start/img/build_log.png
+++ b/doc/ci/quick_start/img/build_log.png
Binary files differ
diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png
index b8e6c2a361a..47862761ffe 100644
--- a/doc/ci/quick_start/img/builds_status.png
+++ b/doc/ci/quick_start/img/builds_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/new_commit.png b/doc/ci/quick_start/img/new_commit.png
index 3d3c9d5c0bd..a53562ce328 100644
--- a/doc/ci/quick_start/img/new_commit.png
+++ b/doc/ci/quick_start/img/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png
new file mode 100644
index 00000000000..6bc97bb739c
--- /dev/null
+++ b/doc/ci/quick_start/img/pipelines_status.png
Binary files differ
diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png
index eafcfd6ecd5..23261123b18 100644
--- a/doc/ci/quick_start/img/runners_activated.png
+++ b/doc/ci/quick_start/img/runners_activated.png
Binary files differ
diff --git a/doc/ci/quick_start/img/single_commit_status_pending.png b/doc/ci/quick_start/img/single_commit_status_pending.png
index 23b3bb5acfc..ccf3ac957bb 100644
--- a/doc/ci/quick_start/img/single_commit_status_pending.png
+++ b/doc/ci/quick_start/img/single_commit_status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/img/status_pending.png b/doc/ci/quick_start/img/status_pending.png
index a049ec2a5ba..9feacf0c961 100644
--- a/doc/ci/quick_start/img/status_pending.png
+++ b/doc/ci/quick_start/img/status_pending.png
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index a06650b3387..ddebd987650 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -63,10 +63,10 @@ instance.
Now simply register the runner as any runner:
```
-sudo gitlab-runner register
+sudo gitlab-ci-multi-runner register
```
-Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
+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.
@@ -93,9 +93,15 @@ setup a specific runner for this project.
To register the runner, run the command below and follow instructions:
```
-sudo gitlab-runner register
+sudo gitlab-ci-multi-runner register
```
+### Lock a specific runner from being enabled for other projects
+
+You can configure a runner to assign it exclusively to a project. When a
+runner is locked this way, it can no longer be enabled for other projects.
+This setting is available on each runner in *Project Settings* > *Runners*.
+
### Making an existing Shared Runner Specific
If you are an admin on your GitLab instance,
@@ -125,7 +131,13 @@ shared runners will only run the jobs they are equipped to run.
For instance, at GitLab we have runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites.
-### Be Careful with Sensitive Information
+### Prevent runner with tags from picking jobs without tags
+
+You can configure a runner to prevent it from picking jobs with tags when
+the runner does not have tags assigned. This setting is available on each
+runner in *Project Settings* > *Runners*.
+
+### Be careful with sensitive information
If you can run a build on a runner, you can get access to any code it runs
and get the token of the runner. With shared runners, this means that anyone
diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png
index f51ea694e78..c812defa67b 100644
--- a/doc/ci/runners/project_specific.png
+++ b/doc/ci/runners/project_specific.png
Binary files differ
diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png
index 9755144eb08..31574a17764 100644
--- a/doc/ci/runners/shared_runner.png
+++ b/doc/ci/runners/shared_runner.png
Binary files differ
diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png
index 44a4bef22f7..8f4010a5849 100644
--- a/doc/ci/runners/shared_to_specific_admin.png
+++ b/doc/ci/runners/shared_to_specific_admin.png
Binary files differ
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 79ed512aabb..5c316510d0e 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds
The required parameters are the trigger's `token` and the Git `ref` on which
the trigger will be performed. Valid refs are the branch, the tag or the commit
-SHA. The `:id` of a project can be found by [querying the API](../api/projects.md)
+SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index e78794fbee7..2dee8ee6107 100644
--- a/doc/ci/triggers/img/builds_page.png
+++ b/doc/ci/triggers/img/builds_page.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index c25f27409d6..baf3fc183d8 100644
--- a/doc/ci/triggers/img/trigger_single_build.png
+++ b/doc/ci/triggers/img/trigger_single_build.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 2207e8b34cb..908355c33a5 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 268368dc3c5..69cec5cdebf 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 70fb81492d6..137b080a8f7 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -34,6 +34,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **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_TOKEN** | 1.2 | Token used for authenticating with the GitLab Container Registry |
| **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 |
@@ -50,6 +51,7 @@ export CI_BUILD_TAG="1.0.0"
export CI_BUILD_NAME="spec:other"
export CI_BUILD_STAGE="test"
export CI_BUILD_TRIGGERED="true"
+export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_ID="34"
export CI_SERVER="yes"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7e9bced7616..eb81267242e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -13,29 +13,34 @@ If you want a quick introduction to GitLab CI, follow our
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [.gitlab-ci.yml](#gitlab-ci-yml)
- - [image and services](#image-and-services)
- - [before_script](#before_script)
- - [after_script](#after_script)
- - [stages](#stages)
- - [types](#types)
- - [variables](#variables)
- - [cache](#cache)
- - [cache:key](#cache-key)
+ - [image and services](#image-and-services)
+ - [before_script](#before_script)
+ - [after_script](#after_script)
+ - [stages](#stages)
+ - [types](#types)
+ - [variables](#variables)
+ - [cache](#cache)
+ - [cache:key](#cache-key)
- [Jobs](#jobs)
- - [script](#script)
- - [stage](#stage)
- - [job variables](#job-variables)
- - [only and except](#only-and-except)
- - [tags](#tags)
- - [when](#when)
- - [artifacts](#artifacts)
- - [artifacts:name](#artifacts-name)
- - [dependencies](#dependencies)
- - [before_script and after_script](#before_script-and-after_script)
+ - [script](#script)
+ - [stage](#stage)
+ - [only and except](#only-and-except)
+ - [job variables](#job-variables)
+ - [tags](#tags)
+ - [when](#when)
+ - [environment](#environment)
+ - [artifacts](#artifacts)
+ - [artifacts:name](#artifactsname)
+ - [artifacts:when](#artifactswhen)
+ - [artifacts:expire_in](#artifactsexpire_in)
+ - [dependencies](#dependencies)
+ - [before_script and after_script](#before_script-and-after_script)
+- [Git Strategy](#git-strategy)
+- [Shallow cloning](#shallow-cloning)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
+ - [Anchors](#anchors)
+- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ciyml)
- [Skipping builds](#skipping-builds)
- [Examples](#examples)
@@ -51,7 +56,7 @@ of your repository and contains definitions of how your project should be built.
The YAML file defines a set of jobs with constraints stating when they should
be run. The jobs are defined as top-level elements with a name and always have
-to contain the `script` clause:
+to contain at least the `script` clause:
```yaml
job1:
@@ -128,7 +133,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
>**Note:**
-Introduced in GitLab 8.7 and GitLab Runner v1.2.
+Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released)
`after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string.
@@ -162,9 +167,9 @@ stages:
There are also two edge cases worth mentioning:
-1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`,
+1. If no `stages` are defined in `.gitlab-ci.yml`, then by default the `build`,
`test` and `deploy` are allowed to be used as job's stage by default.
-2. If a job doesn't specify `stage`, the job is assigned the `test` stage.
+2. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
### types
@@ -175,9 +180,9 @@ Alias for [stages](#stages).
>**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
-store non-sensitive project configuration, for example:
+GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
+build environment. The variables are stored in the git repository and are meant
+to store non-sensitive project configuration, for example:
```yaml
variables:
@@ -250,8 +255,8 @@ rspec:
- 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.
+The cache is provided on a best-effort basis, so don't expect that the cache
+will be always present. For implementation details, please check GitLab Runner.
#### cache:key
@@ -348,10 +353,11 @@ job_name:
| 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 |
+| artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
| before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build |
+| environment | no | Defines a name of environment to which deployment is done by this build |
### script
@@ -475,10 +481,10 @@ failure.
`when` can be set to one of the following values:
1. `on_success` - execute build only when all builds from prior stages
- succeeded. This is the default.
+ succeed. This is the default.
1. `on_failure` - execute build only when at least one build from prior stages
- failed.
-1. `always` - execute build despite the status of builds from prior stages.
+ fails.
+1. `always` - execute build regardless of the status of builds from prior stages.
For example:
@@ -523,6 +529,36 @@ The above script will:
1. Execute `cleanup_build_job` only when `build_job` fails
2. Always execute `cleanup_job` as the last step in pipeline.
+### environment
+
+>**Note:**
+Introduced in GitLab 8.9.
+
+`environment` is used to define that a job deploys to a specific environment.
+This allows easy tracking of all deployments to your environments straight from
+GitLab.
+
+If `environment` is specified and no environment under that name exists, a new
+one will be created automatically.
+
+The `environment` name must contain only letters, digits, '-' and '_'. Common
+names are `qa`, `staging`, and `production`, but you can use whatever name works
+with your workflow.
+
+---
+
+**Example configurations**
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment: production
+```
+
+The `deploy to production` job will be marked as doing deployment to
+`production` environment.
+
### artifacts
>**Notes:**
@@ -530,10 +566,10 @@ The above script will:
> - 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.
+> - Build artifacts are only collected for successful builds by default.
-`artifacts` is used to specify list of files and directories which should be
-attached to build after success. To pass artifacts between different builds,
+`artifacts` is used to specify a list of files and directories which should be
+attached to the build after success. To pass artifacts between different builds,
see [dependencies](#dependencies).
Below are some examples.
@@ -651,6 +687,68 @@ job:
untracked: true
```
+#### artifacts:when
+
+>**Note:**
+Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+
+`artifacts:when` is used to upload artifacts on build failure or despite the
+failure.
+
+`artifacts:when` can be set to one of the following values:
+
+1. `on_success` - upload artifacts only when the build succeeds. This is the default.
+1. `on_failure` - upload artifacts only when the build fails.
+1. `always` - upload artifacts regardless of the build status.
+
+---
+
+**Example configurations**
+
+To upload artifacts only when build fails.
+
+```yaml
+job:
+ artifacts:
+ when: on_failure
+```
+
+#### artifacts:expire_in
+
+>**Note:**
+Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+
+`artifacts:expire_in` is used to delete uploaded artifacts after the specified
+time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
+to specify how long artifacts should live before they expire, counting from the
+time they are uploaded and stored on GitLab.
+
+You can use the **Keep** button on the build page to override expiration and
+keep artifacts forever.
+
+After expiry, artifacts are actually deleted hourly by default (via a cron job),
+but they are not accessible after expiry.
+
+The value of `expire_in` is an elapsed time. Examples of parseable values:
+- '3 mins 4 sec'
+- '2 hrs 20 min'
+- '2h20min'
+- '6 mos 1 day'
+- '47 yrs 6 mos and 4d'
+- '3 weeks and 2 days'
+
+---
+
+**Example configurations**
+
+To expire artifacts 1 week after being uploaded:
+
+```yaml
+job:
+ artifacts:
+ expire_in: 1 week
+```
+
### dependencies
>**Note:**
@@ -713,7 +811,7 @@ deploy:
It's possible to overwrite globally defined `before_script` and `after_script`:
```yaml
-before_script
+before_script:
- global before script
job:
@@ -725,6 +823,61 @@ job:
- execute this after my script
```
+## Git Strategy
+
+>**Note:**
+Introduced in GitLab 8.9 as an experimental feature. May change in future
+releases or be removed completely.
+
+You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
+is slower, but makes sure you have a clean directory before every build. `fetch`
+is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
+in the `variables` section for individual jobs. If it's not specified, then the
+default from project settings will be used.
+
+```
+variables:
+ GIT_STRATEGY: clone
+```
+
+or
+
+```
+variables:
+ GIT_STRATEGY: fetch
+```
+
+## Shallow cloning
+
+>**Note:**
+Introduced in GitLab 8.9 as an experimental feature. May change in future
+releases or be removed completely.
+
+You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
+shallow cloning of the repository which can significantly speed up cloning for
+repositories with a large number of commits or old, large binaries. The value is
+passed to `git fetch` and `git clone`.
+
+>**Note:**
+If you use a depth of 1 and have a queue of builds or retry
+builds, jobs may fail.
+
+Since Git fetching and cloning is based on a ref, such as a branch name, runners
+can't clone a specific commit SHA. If there are multiple builds in the queue, or
+you are retrying an old build, the commit to be tested needs to be within the
+git history that is cloned. Setting too small a value for `GIT_DEPTH` can make
+it impossible to run these old commits. You will see `unresolved reference` in
+build logs. You should then reconsider changing `GIT_DEPTH` to a higher value.
+
+Builds that rely on `git describe` may not work correctly when `GIT_DEPTH` is
+set since only part of the git history is present.
+
+To fetch or clone only the last 3 commits:
+```
+variables:
+ GIT_DEPTH: "3"
+```
+
## Hidden jobs
>**Note:**
@@ -881,8 +1034,8 @@ You can find the link under `/ci/lint` of your gitlab instance.
## Skipping builds
-If your commit message contains `[ci skip]`, the commit will be created but the
-builds will be skipped.
+If your commit message contains `[ci skip]` or `[skip ci]`, using any
+capitalization, the commit will be created but the builds will be skipped.
## Examples
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
new file mode 100644
index 00000000000..1b465434498
--- /dev/null
+++ b/doc/container_registry/README.md
@@ -0,0 +1,94 @@
+# GitLab Container Registry
+
+> **Note:**
+This feature was [introduced][ce-4040] in GitLab 8.8.
+
+> **Note:**
+This document is about the user guide. To learn how to enable GitLab Container
+Registry across your GitLab instance, visit the
+[administrator documentation](../administration/container_registry.md).
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+## Enable the Container Registry for your project
+
+1. First, ask your system administrator to enable GitLab Container Registry
+ following the [administration documentation](../administration/container_registry.md).
+ If you are using GitLab.com, this is enabled by default so you can start using
+ the Registry immediately.
+
+1. Go to your project's settings and enable the **Container Registry** feature
+ on your project. For new projects this might be enabled by default. For
+ existing projects you will have to explicitly enable it.
+
+ ![Enable Container Registry](img/project_feature.png)
+
+## Build and push images
+
+After you save your project's settings, you should see a new link in the
+sidebar called **Container Registry**. Following this link will get you to
+your project's Registry panel where you can see how to login to the Container
+Registry using your GitLab credentials.
+
+For example if the Registry's URL is `registry.example.com`, the you should be
+able to login with:
+
+```
+docker login registry.example.com
+```
+
+Building and publishing images should be a straightforward process. Just make
+sure that you are using the Registry URL with the namespace and project name
+that is hosted on GitLab:
+
+```
+docker build -t registry.example.com/group/project .
+docker push registry.example.com/group/project
+```
+
+## Use images from GitLab Container Registry
+
+To download and run a container from images hosted in GitLab Container Registry,
+use `docker run`:
+
+```
+docker run [options] registry.example.com/group/project [arguments]
+```
+
+For more information on running Docker containers, visit the
+[Docker documentation][docker-docs].
+
+## Control Container Registry from within GitLab
+
+GitLab offers a simple Container Registry management panel. Go to your project
+and click **Container Registry** in the left sidebar.
+
+This view will show you all tags in your project and will easily allow you to
+delete them.
+
+![Container Registry panel](img/container_registry.png)
+
+## Build and push images using GitLab CI
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Make sure that your GitLab Runner is configured to allow building docker images.
+You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
+Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+
+## Limitations
+
+In order to use a container image from your private project as an `image:` in
+your `.gitlab-ci.yml`, you have to follow the
+[Using a private Docker Registry][private-docker]
+documentation. This workflow will be simplified in the future.
+
+[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
+[docker-docs]: https://docs.docker.com/engine/userguide/intro/
+[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png
new file mode 100644
index 00000000000..57d6f9f22c5
--- /dev/null
+++ b/doc/container_registry/img/container_registry.png
Binary files differ
diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png
new file mode 100644
index 00000000000..a59b4f82b56
--- /dev/null
+++ b/doc/container_registry/img/project_feature.png
Binary files differ
diff --git a/doc/customization/branded_login_page/appearance.png b/doc/customization/branded_login_page/appearance.png
index 6bce1f0a287..023dc5599b4 100644
--- a/doc/customization/branded_login_page/appearance.png
+++ 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
index d6020b029a2..7d99e0a2b3b 100644
--- a/doc/customization/branded_login_page/custom_sign_in.png
+++ 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
index 795c7954d8e..0cfa9da202e 100644
--- a/doc/customization/branded_login_page/default_login_page.png
+++ 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 194b8e00299..4620bb2dcde 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -8,7 +8,7 @@ the matched text will be closed. This happens when the commit is pushed to a pro
When not specified, the default `issue_closing_pattern` as shown below will be used:
```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)
+((?:[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+))+)
```
Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`).
diff --git a/doc/development/README.md b/doc/development/README.md
index aa7d54c01d0..c5d5af43864 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -7,6 +7,7 @@
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md)
+- [Licensing](licensing.md) for ensuring license compliance
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Performance guidelines](performance.md)
- [Rake tasks](rake_tasks.md) for development
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 12e33406cb6..33fd50f4c11 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -52,7 +52,9 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
-![GitLab Diagram Overview](gitlab_diagram_overview.png)
+![GitLab Diagram Overview](gitlab_architecture_diagram.png)
+
+_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index 6776d9b083f..2f49b3564ab 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -21,7 +21,7 @@ We currently use three CI services to test GitLab:
Core team has access to trigger builds if needed for GitLab CE.
-We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/build_script_gitlab_ce.md) for testing with GitLab CI.
+We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) for testing with GitLab CI.
# Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 187ec9e7b75..975bb82c37d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -103,14 +103,14 @@ Inside the document:
- 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_`
+ 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
+ `>**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
+ `>**Note:** This feature was introduced in GitLab EE 8.3`. Otherwise, leave
this mention out
## References
@@ -127,7 +127,7 @@ Inside the document:
```
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`.
+ `http://docs.gitlab.com/ce/administration/restart_gitlab.html`.
Replace `reconfigure` with `restart` where appropriate.
## Installation guide
@@ -141,6 +141,104 @@ Inside the document:
[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
+## Changing document location
+
+Changing a document's location is not to be taken lightly. Remember that the
+documentation is available to all installations under `help/` and not only to
+GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
+Documentation team beforehand.
+
+If you indeed need to change a document's location, do NOT remove the old
+document, but rather put a text in it that points to the new location, like:
+
+```
+This document was moved to [path/to/new_doc.md](path/to/new_doc.md).
+```
+
+where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
+
+---
+
+For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
+`doc/administration/lfs.md`, then the steps would be:
+
+1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
+1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
+
+ ```
+ This document was moved to [administration/lfs.md](../../administration/lfs.md).
+ ```
+
+1. Find and replace any occurrences of the old location with the new one.
+ A quick way to find them is to use `grep`:
+
+ ```
+ grep -nR "lfs_administration.md" doc/
+ ```
+
+ The above command will search in the `doc/` directory for
+ `lfs_administration.md` recursively and will print the file and the line
+ where this file is mentioned. Note that we used just the filename
+ (`lfs_administration.md`) and not the whole the relative path
+ (`workflow/lfs/lfs_administration.md`).
+
+
+## Configuration documentation for source and Omnibus installations
+
+GitLab currently officially supports two installation methods: installations
+from source and Omnibus packages installations.
+
+Whenever there is a setting that is configurable for both installation methods,
+prefer to document it in the CE docs to avoid duplication.
+
+Configuration settings include:
+
+- settings that touch configuration files in `config/`
+- NGINX settings and settings in `lib/support/` in general
+
+When there is a list of steps to perform, usually that entails editing the
+configuration file and reconfiguring/restarting GitLab. In such case, follow
+the style below as a guide:
+
+````
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ external_url "https://gitlab.example.com"
+ ```
+
+1. Save the file and [reconfigure] GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ host: "gitlab.example.com"
+ ```
+
+1. Save the file and [restart] GitLab for the changes to take effect.
+
+
+[reconfigure]: path/to/administration/gitlab_restart.md#omnibus-gitlab-reconfigure
+[restart]: path/to/administration/gitlab_restart.md#installations-from-source
+````
+
+In this case:
+
+- before each step list the installation method is declared in bold
+- three dashes (`---`) are used to create an horizontal line and separate the
+ two methods
+- the code blocks are indented one or more spaces under the list item to render
+ correctly
+- different highlighting languages are used for each config in the code block
+- the [references](#references) guide is used for reconfigure/restart
+
## API
Here is a list of must-have items. Use them in the exact order that appears
@@ -222,8 +320,8 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.
#### Post data using JSON content
-_**Note:** In this example we create a new group. Watch carefully the single
-and double quotes._
+> **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
@@ -266,5 +364,5 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai
[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"
+[gfm]: http://docs.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/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png
new file mode 100644
index 00000000000..80e975718e0
--- /dev/null
+++ b/doc/development/gitlab_architecture_diagram.png
Binary files differ
diff --git a/doc/development/gitlab_diagram_overview.png b/doc/development/gitlab_diagram_overview.png
deleted file mode 100644
index d9b9eed3d8f..00000000000
--- a/doc/development/gitlab_diagram_overview.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 21078c8d6f9..9d7fe7440d2 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -46,7 +46,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
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)
+_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
in an initializer._
### Further reading
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 9168c70945a..c2272ab0a2b 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -15,8 +15,8 @@ instrument code:
* `instrument_instance_method`: instruments a single instance method.
* `instrument_class_hierarchy`: given a Class this method will recursively
instrument all sub-classes (both class and instance methods).
-* `instrument_methods`: instruments all public class methods of a Module.
-* `instrument_instance_methods`: instruments all public instance methods of a
+* `instrument_methods`: instruments all public and private class methods of a Module.
+* `instrument_instance_methods`: instruments all public and private instance methods of a
Module.
To remove the need for typing the full `Gitlab::Metrics::Instrumentation`
@@ -94,22 +94,8 @@ Visibility: public
Number of lines: 21
def #{name}(#{args_signature})
- trans = Gitlab::Metrics::Instrumentation.transaction
-
- if trans
- start = Time.now
- retval = super
- 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})
- end
-
- retval
+ if trans = Gitlab::Metrics::Instrumentation.transaction
+ trans.measure_method(#{label.inspect}) { super }
else
super
end
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
new file mode 100644
index 00000000000..8c8c7486fff
--- /dev/null
+++ b/doc/development/licensing.md
@@ -0,0 +1,93 @@
+# GitLab Licensing and Compatibility
+
+GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed under "The GitLab Enterprise Edition (EE) license" wherein there are more restrictions. See their respective LICENSE files ([CE][CE], [EE][EE]) for more information.
+
+## Automated Testing
+
+In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
+
+There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
+
+Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually.
+
+### License Finder commands
+
+There are a few basic commands License Finder provides that you'll need in order to manage license detection.
+
+To verify that the checks are passing, and/or to see what dependencies are causing the checks to fail:
+
+```
+bundle exec license_finder
+```
+
+To whitelist a new license:
+
+```
+license_finder whitelist add MIT
+```
+
+To blacklist a new license:
+
+```
+license_finder blacklist add GPLv2
+```
+
+To tell License Finder about a dependency's license if it isn't auto-detected:
+
+```
+license_finder licenses add my_unknown_dependency MIT
+```
+
+For all of the above, please include `--why "Reason"` and `--who "My Name"` so the `decisions.yml` file can keep track of when, why, and who approved of a dependency.
+
+More detailed information on how the gem and its commands work is available in the [License Finder README][license_finder].
+
+## Acceptable Licenses
+
+Libraries with the following licenses are acceptable for use:
+
+- [The MIT License][MIT] (the MIT Expat License specifically): The MIT License requires that the license itself is included with all copies of the source. It is a permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [LGPL][LGPL] (version 2, version 3): GPL constraints regarding modification and redistribution under the same license are not required of projects using an LGPL library, only upon modification of the LGPL-licensed library itself.
+- [Apache 2.0 License][apache-2]: A permissive license that also provides an express grant of patent rights from contributors to users.
+- [Ruby 1.8 License][ruby-1.8]: Dual-licensed under either itself or the GPLv2, defer to the Ruby License itself. Acceptable because of point 3b: "You may distribute the software in object code or binary form, provided that you do at least ONE of the following: b) accompany the distribution with the machine-readable source of the software."
+- [Ruby 1.9 License][ruby-1.9]: Dual-licensed under either itself or the BSD 2-Clause License, defer to BSD 2-Clause.
+- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
+- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
+
+## Unacceptable Licenses
+
+Libraries with the following licenses are unacceptable for use:
+
+- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
+- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
+
+## Notes
+
+Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL.
+
+If a gem uses a license which is not listed above, open an issue and ask. If a license is not included in the "acceptable" list, operate under the assumption that it is not acceptable.
+
+Keep in mind that each license has its own restrictions (typically defined in their body text). Please make sure to comply with those restrictions at all times whenever an external library is used.
+
+Gems which are included only in the "development" or "test" groups by Bundler are exempt from license requirements, as they're not distributed for use in production.
+
+**NOTE:** This document is **not** legal advice, nor is it comprehensive. It should not be taken as such.
+
+[CE]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE
+[EE]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/LICENSE
+[license_finder]: https://github.com/pivotal/LicenseFinder
+[MIT]: http://choosealicense.com/licenses/mit/
+[LGPL]: http://choosealicense.com/licenses/lgpl-3.0/
+[apache-2]: http://choosealicense.com/licenses/apache-2.0/
+[ruby-1.8]: https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING
+[ruby-1.9]: https://www.ruby-lang.org/en/about/license.txt
+[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
+[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
+[ISC]: https://opensource.org/licenses/ISC
+[GPL]: http://choosealicense.com/licenses/gpl-3.0/
+[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt
+[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt
+[AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/
+[GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL
+[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 28dedf3978c..e2ca46504e7 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -8,7 +8,10 @@ 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.
+Migrations should not require GitLab installations to be taken offline unless
+_absolutely_ necessary. If a migration requires downtime this should be
+clearly mentioned during the review process as well as being documented in the
+monthly release post.
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
@@ -31,6 +34,14 @@ First, you need to provide information on whether the migration can be applied:
3. online with errors on new instances while migrating
4. offline (needs to happen without app servers to prevent db corruption)
+For example:
+
+```
+# Migration type: online without errors (works on previous version and new one)
+class MyMigration < ActiveRecord::Migration
+...
+```
+
It is always preferable to have a migration run online. If you expect the migration
to take particularly long (for instance, if it loops through all notes),
this is valuable information to add.
@@ -45,7 +56,6 @@ be possible to downgrade in case of a vulnerability or bugs.
In your migration, add a comment describing how the reversibility of the
migration was tested.
-
## Removing indices
If you need to remove index, please add a condition like in following example:
@@ -58,6 +68,49 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation.
+When adding an index make sure to use the method `add_concurrent_index` instead
+of the regular `add_index` method. The `add_concurrent_index` method
+automatically creates concurrent indexes when using PostgreSQL, removing the
+need for downtime. To use this method you must disable transactions by calling
+the method `disable_ddl_transaction!` in the body of your migration class like
+so:
+
+```
+class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def change
+
+ end
+end
+```
+
+## Adding Columns With Default Values
+
+When adding columns with default values you should use the method
+`add_column_with_default`. This method ensures the table is updated without
+requiring downtime. This method is not reversible so you must manually define
+the `up` and `down` methods in your migration class.
+
+For example, to add the column `foo` to the `projects` table with a default
+value of `10` you'd write the following:
+
+```
+class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:projects, :foo, :integer, default: 10)
+ end
+
+ def down
+ remove_column(:projects, :foo)
+ end
+end
+```
+
## Testing
Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
@@ -74,7 +127,7 @@ Example with Arel:
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
-#updtae other tables with this results
+#update other tables with these results
```
Example with plain SQL and `quote_string` helper:
@@ -89,4 +142,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/rake_tasks.md b/doc/development/rake_tasks.md
index 6d04b9590e6..41685c7ee41 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -33,3 +33,23 @@ bundle exec rake gitlab:generate_docs
```
bundle exec rake services:doc
```
+
+## Updating Emoji Digests
+
+To update the Emoji digests file (used for Emoji autocomplete) you must run the
+following:
+
+```
+bundle exec rake gemojione:digests
+```
+
+This will update the file `fixtures/emojis/digests.json` based on the currently
+available Emoji.
+
+## Emoji Sprites
+
+Generating a sprite file containing all the Emoji can be done by running:
+
+```
+bundle exec rake gemojione:sprite
+```
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 33eed29ba5c..513457d203a 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -65,7 +65,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at
- Use `context` to test branching logic.
- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)).
- Don't supply the `:each` argument to hooks since it's the default.
-- Prefer `not_to` to `to_not`.
+- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index a3e260a5f89..5893b7c219e 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -6,3 +6,51 @@ We created a page inside GitLab where you can check commonly used html and css e
When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples
you can use during GitLab development.
+
+## Design repository
+
+All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
+repository and maintained by GitLab UX designers.
+
+## Navigation
+
+GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
+This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo
+and the current user's profile picture. The content section contains a header and the content itself.
+The header describes the current GitLab page and what navigation is
+available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the
+project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
+
+### Adding new tab to header navigation
+
+We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each
+tab should represent separate functionality. Everything related to the issue
+tracker should be under the 'Issues' tab while everything related to the wiki should
+be under 'Wiki' tab and so on and so forth.
+
+## Mobile screen size
+
+We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide
+part of the UI for smaller resolutions in favor of a better user experience.
+However core functionality like browsing files, creating issues, writing comments, should
+be available on all resolutions.
+
+## Icons
+
+* `trash` icon for button or link that does destructive action like removing
+information from database or file system
+* `x` icon for closing/hiding UI element. For example close modal window
+* `pencil` icon for edit button or link
+* `eye` icon for subscribe action
+* `rss` for rss/atom feed
+* `plus` for link or dropdown that lead to page where you create new object (For example new issue page)
+
+
+## Buttons
+
+* Button should contain icon or text. Exceptions should be approved by UX designer.
+* Use red button for destructive actions (not revertable). For example removing issue.
+* Use green or blue button for primary action. Primary button should be only one.
+Do not use both green and blue button in one form.
+* For all other cases use default white button
+
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index 3625c4191b8..a6d22e5a04a 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -44,13 +44,13 @@ to avoid getting this error, you need to remove all instances of the
**Omnibus Installation**
```
-$ sudo gitlab-rails runner "Service.where(type: 'JenkinsService').delete_all"
+$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all"
```
**Source Installation**
```
-$ bundle exec rails runner "Service.where(type: 'JenkinsService').delete_all" production
+$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production
```
## Downgrade to CE
diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
index 9d93b217a59..e60992c4c6a 100644
--- a/doc/gitlab-basics/basicsimages/add_new_merge_request.png
+++ b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png
index 2dede97aa40..89c86018629 100644
--- a/doc/gitlab-basics/basicsimages/add_sshkey.png
+++ b/doc/gitlab-basics/basicsimages/add_sshkey.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png
index c5e38b552a5..2264f3c5bf2 100644
--- a/doc/gitlab-basics/basicsimages/branch_info.png
+++ b/doc/gitlab-basics/basicsimages/branch_info.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png
index 06e77f5eea9..75fe8313611 100644
--- a/doc/gitlab-basics/basicsimages/branch_name.png
+++ b/doc/gitlab-basics/basicsimages/branch_name.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png
index c18fa83b968..8621bc05776 100644
--- a/doc/gitlab-basics/basicsimages/branches.png
+++ b/doc/gitlab-basics/basicsimages/branches.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png
index 457af459bb9..b52ab148839 100644
--- a/doc/gitlab-basics/basicsimages/button-create-mr.png
+++ b/doc/gitlab-basics/basicsimages/button-create-mr.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png
index 94b6d5756d3..6450deec6fc 100644
--- a/doc/gitlab-basics/basicsimages/click-on-new-group.png
+++ b/doc/gitlab-basics/basicsimages/click-on-new-group.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png
index 81588336f37..a88809c5a3f 100644
--- a/doc/gitlab-basics/basicsimages/commit_changes.png
+++ b/doc/gitlab-basics/basicsimages/commit_changes.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png
index 0df2c32653c..4abe4517f98 100644
--- a/doc/gitlab-basics/basicsimages/commit_message.png
+++ b/doc/gitlab-basics/basicsimages/commit_message.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png
index 7e606539077..2bfcaf75f01 100644
--- a/doc/gitlab-basics/basicsimages/commits.png
+++ b/doc/gitlab-basics/basicsimages/commits.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/compare_branches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
index 7eebaed9075..8a18453dd05 100644
--- a/doc/gitlab-basics/basicsimages/compare_branches.png
+++ b/doc/gitlab-basics/basicsimages/compare_branches.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png
index 688e355cca2..5ebe1b227dd 100644
--- a/doc/gitlab-basics/basicsimages/create_file.png
+++ b/doc/gitlab-basics/basicsimages/create_file.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png
index 57da898abdc..7ecc3baa990 100644
--- a/doc/gitlab-basics/basicsimages/create_group.png
+++ b/doc/gitlab-basics/basicsimages/create_group.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png
index afa68760108..9d3e817d036 100644
--- a/doc/gitlab-basics/basicsimages/edit_file.png
+++ b/doc/gitlab-basics/basicsimages/edit_file.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png
index 1def489d16b..e357cb5c6ab 100644
--- a/doc/gitlab-basics/basicsimages/file_located.png
+++ b/doc/gitlab-basics/basicsimages/file_located.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png
index 9ac2f1c355f..01639c77d0d 100644
--- a/doc/gitlab-basics/basicsimages/file_name.png
+++ b/doc/gitlab-basics/basicsimages/file_name.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png
index 98639149a39..6f26d26ae18 100644
--- a/doc/gitlab-basics/basicsimages/find_file.png
+++ b/doc/gitlab-basics/basicsimages/find_file.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png
index 5ac33c7e953..1211510aae9 100644
--- a/doc/gitlab-basics/basicsimages/find_group.png
+++ b/doc/gitlab-basics/basicsimages/find_group.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png
index b1f94938613..13ff8345616 100644
--- a/doc/gitlab-basics/basicsimages/fork.png
+++ b/doc/gitlab-basics/basicsimages/fork.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png
index e78d84e4d80..2507d6c295b 100644
--- a/doc/gitlab-basics/basicsimages/group_info.png
+++ b/doc/gitlab-basics/basicsimages/group_info.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png
index b8104343afa..ef3dca60cc8 100644
--- a/doc/gitlab-basics/basicsimages/groups.png
+++ b/doc/gitlab-basics/basicsimages/groups.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png
index 2a31b4cf751..e74dbc13f9a 100644
--- a/doc/gitlab-basics/basicsimages/https.png
+++ b/doc/gitlab-basics/basicsimages/https.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png
index 1061d9c5082..7f304b8e1f2 100644
--- a/doc/gitlab-basics/basicsimages/image_file.png
+++ b/doc/gitlab-basics/basicsimages/image_file.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png
index 7b69c705392..60a6f7973be 100644
--- a/doc/gitlab-basics/basicsimages/issue_title.png
+++ b/doc/gitlab-basics/basicsimages/issue_title.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png
index 9354d05319e..14e9cdb64e1 100644
--- a/doc/gitlab-basics/basicsimages/issues.png
+++ b/doc/gitlab-basics/basicsimages/issues.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png
index 321805cda98..04400173ce8 100644
--- a/doc/gitlab-basics/basicsimages/key.png
+++ b/doc/gitlab-basics/basicsimages/key.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png
index 7601d40de47..570164df18b 100644
--- a/doc/gitlab-basics/basicsimages/merge_requests.png
+++ b/doc/gitlab-basics/basicsimages/merge_requests.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png
index 9120d2b1ab1..842f5ebed74 100644
--- a/doc/gitlab-basics/basicsimages/new_merge_request.png
+++ b/doc/gitlab-basics/basicsimages/new_merge_request.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png
index ac255270a66..421e8bc247b 100644
--- a/doc/gitlab-basics/basicsimages/new_project.png
+++ b/doc/gitlab-basics/basicsimages/new_project.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png
index da1a6b604ea..d5fcf33c4ea 100644
--- a/doc/gitlab-basics/basicsimages/newbranch.png
+++ b/doc/gitlab-basics/basicsimages/newbranch.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png
index 9880ddfead1..578ebee4440 100644
--- a/doc/gitlab-basics/basicsimages/paste_sshkey.png
+++ b/doc/gitlab-basics/basicsimages/paste_sshkey.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png
index 5f2e7a7e10c..cb3f79f1879 100644
--- a/doc/gitlab-basics/basicsimages/profile_settings.png
+++ b/doc/gitlab-basics/basicsimages/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png
index 6c06ff351fa..e1adb8d48c2 100644
--- a/doc/gitlab-basics/basicsimages/project_info.png
+++ b/doc/gitlab-basics/basicsimages/project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/basicsimages/public_file_link.png
index 1a60a3d880a..f60df6807f4 100644
--- a/doc/gitlab-basics/basicsimages/public_file_link.png
+++ b/doc/gitlab-basics/basicsimages/public_file_link.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png
index d02c2255ff2..33b978dd899 100644
--- a/doc/gitlab-basics/basicsimages/select-group.png
+++ b/doc/gitlab-basics/basicsimages/select-group.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png
index fd40bce499b..aee22c638db 100644
--- a/doc/gitlab-basics/basicsimages/select-group2.png
+++ b/doc/gitlab-basics/basicsimages/select-group2.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png
index 3475b2df576..f72a3ffb57f 100644
--- a/doc/gitlab-basics/basicsimages/select_branch.png
+++ b/doc/gitlab-basics/basicsimages/select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png
index 6d5aa439124..3bb832ea8d0 100644
--- a/doc/gitlab-basics/basicsimages/select_project.png
+++ b/doc/gitlab-basics/basicsimages/select_project.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png
index 9bf9c5a0d39..78637013d9b 100644
--- a/doc/gitlab-basics/basicsimages/settings.png
+++ b/doc/gitlab-basics/basicsimages/settings.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png
index d7ef4dafe77..c87f11a9d3d 100644
--- a/doc/gitlab-basics/basicsimages/shh_keys.png
+++ b/doc/gitlab-basics/basicsimages/shh_keys.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png
index 18944417085..78b854c8903 100644
--- a/doc/gitlab-basics/basicsimages/submit_new_issue.png
+++ b/doc/gitlab-basics/basicsimages/submit_new_issue.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png
index e08eb628414..c31d61ec336 100644
--- a/doc/gitlab-basics/basicsimages/title_description_mr.png
+++ b/doc/gitlab-basics/basicsimages/title_description_mr.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png
index 6363a09360e..eaa969bdcf4 100644
--- a/doc/gitlab-basics/basicsimages/white_space.png
+++ b/doc/gitlab-basics/basicsimages/white_space.png
Binary files differ
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 87f078def04..5221d85b661 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all
![Submit new issue](basicsimages/submit_new_issue.png)
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html).
+Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html).
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index b545d62549d..f737dffc024 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -14,7 +14,7 @@ Fill out the required information:
1. Select a [visibility level](https://gitlab.com/help/public_access/public_access)
-1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html)
+1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html)
1. Click on "create project"
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index dcdf49d3379..1d5e5dd6e15 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -1,41 +1,3 @@
# Custom Git Hooks
-**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](../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.
-See
-[Git SCM Server-Side Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks)
-for more information about each hook type.
-
-As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
-administrators can add custom git hooks to any GitLab project.
-
-## Setup
-
-Normally, git hooks are placed in the repository or project's `hooks` directory.
-GitLab creates a symlink from each project's `hooks` directory to the
-gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell
-upgrades. As such, custom hooks are implemented a little differently. Behavior
-is exactly the same once the hook is created, though. Follow these steps to
-set up a custom hook.
-
-1. Pick a project that needs a custom git hook.
-1. On the GitLab server, navigate to the project's repository directory.
-For an installation from source the path is usually
-`/home/git/repositories/<group>/<project>.git`. For Omnibus installs the path is
-usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`.
-1. Create a new directory in this location called `custom_hooks`.
-1. Inside the new `custom_hooks` directory, create a file with a name matching
-the hook type. For a pre-receive hook the file name should be `pre-receive` with
-no extension.
-1. Make the hook file executable and make sure it's owned by git.
-1. Write the code to make the git hook function as expected. Hooks can be
-in any language. Ensure the 'shebang' at the top properly reflects the language
-type. For example, if the script is in Ruby the shebang will probably be
-`#!/usr/bin/env ruby`.
-
-That's it! Assuming the hook code is properly implemented the hook will fire
-as appropriate.
+This document was moved to [administration/custom_hooks.md](../administration/custom_hooks.md).
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index e51ff5a5de2..e8093f0b257 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -36,7 +36,7 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
# Grant the GitLab user necessary permissions on the database
- mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost';
+ mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
# Quit the database session
mysql> \q
diff --git a/doc/install/installation.md b/doc/install/installation.md
index e3af3022262..19d083d580d 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -6,7 +6,7 @@ Since an installation from source is a lot of work and error prone we strongly r
One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes.
On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time.
-Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory.
+Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory.
After this termination Runit will detect Sidekiq is not running and will start it.
Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time.
@@ -269,9 +269,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-7-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-10-stable gitlab
-**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -391,10 +391,14 @@ GitLab Shell is an SSH access and repository management software developed speci
### Install gitlab-workhorse
+GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
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 v0.7.1
+ sudo -u git -H git checkout v0.7.7
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 0245febfcd8..44d2a14f366 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -132,5 +132,5 @@ To disable the relative URL:
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"
+[omnibus-rel]: http://docs.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 df8e8bdc476..a65ac8a5f79 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -52,7 +52,7 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
### CPU
-- 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core
+- 1 core supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core
- **2 cores** is the **recommended** number of cores and supports up to 500 users
- 4 cores supports up to 2,000 users
- 8 cores supports up to 5,000 users
@@ -81,7 +81,7 @@ errors during usage.
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
We recommend having at least 1GB of swap on your server, even if you currently have
-enough available RAM. Having swap will help reduce the chance of errors occuring
+enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
@@ -150,3 +150,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
- Safari 7+ (known problem: required fields in html5 do not work)
- Opera (Latest released version)
- Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled.
+- Edge (Latest stable version)
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 6fe04aa2a06..fd330dd7a7d 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -19,7 +19,7 @@ See the documentation below for details on how to configure these services.
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
-[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
+[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
## Project services
diff --git a/doc/integration/cas.md b/doc/integration/cas.md
index e6b2071f193..e34e306f9ac 100644
--- a/doc/integration/cas.md
+++ b/doc/integration/cas.md
@@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your
```ruby
gitlab_rails['omniauth_providers'] = [
{
- name: "cas3",
- label: "cas",
- args: {
- url: 'CAS_SERVER',
- login_url: '/CAS_PATH/login',
- service_validate_url: '/CAS_PATH/p3/serviceValidate',
- logout_url: '/CAS_PATH/logout'} }
- }
+ "name"=> "cas3",
+ "label"=> "cas",
+ "args"=> {
+ "url"=> 'CAS_SERVER',
+ "login_url"=> '/CAS_PATH/login',
+ "service_validate_url"=> '/CAS_PATH/p3/serviceValidate',
+ "logout_url"=> '/CAS_PATH/logout'
+ }
}
]
```
+
For installations from source:
@@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your
1. Save the configuration file.
+1. Run `gitlab-ctl reconfigure` for the omnibus package.
+
1. Restart GitLab for the changes to take effect.
On the sign in page there should now be a CAS tab in the sign in form.
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index a2d7e922aad..8d2c6351fb8 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -1,7 +1,7 @@
# External issue tracker
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
+Jira, Redmine, or Bugzilla. 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
@@ -20,6 +20,7 @@ Visit the links below for details:
- [Redmine](../project_services/redmine.md)
- [Jira](../project_services/jira.md)
+- [Bugzilla](../project_services/bugzilla.md)
### Service Template
diff --git a/doc/integration/google.md b/doc/integration/google.md
index f9a20dd840d..82978b68a34 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -11,9 +11,9 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
- Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one.
1. Refresh the page. You should now see your new project in the list. Click on the project.
-1. Select "APIs & auth" in the left menu.
+1. Select the "Google APIs" tab in the Overview.
-1. Select "APIs" in the submenu.
+1. Select and enable the following Google APIs - listed under "Popular APIs"
- Enable `Contacts API`
- Enable `Google+ API`
diff --git a/doc/integration/img/akismet_settings.png b/doc/integration/img/akismet_settings.png
index ccdd3adb1c5..c2aa97b132e 100644
--- a/doc/integration/img/akismet_settings.png
+++ b/doc/integration/img/akismet_settings.png
Binary files differ
diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png
new file mode 100644
index 00000000000..b23d6dcc595
--- /dev/null
+++ b/doc/integration/img/enabled-oauth-sign-in-sources.png
Binary files differ
diff --git a/doc/integration/img/facebook_api_keys.png b/doc/integration/img/facebook_api_keys.png
index d6c44ac0f11..995845d5a69 100644
--- a/doc/integration/img/facebook_api_keys.png
+++ b/doc/integration/img/facebook_api_keys.png
Binary files differ
diff --git a/doc/integration/img/facebook_app_settings.png b/doc/integration/img/facebook_app_settings.png
index 30dd21e198a..1cd586ecd7c 100644
--- a/doc/integration/img/facebook_app_settings.png
+++ b/doc/integration/img/facebook_app_settings.png
Binary files differ
diff --git a/doc/integration/img/facebook_website_url.png b/doc/integration/img/facebook_website_url.png
index dc3088bb2fa..10e1bd5d5a6 100644
--- a/doc/integration/img/facebook_website_url.png
+++ b/doc/integration/img/facebook_website_url.png
Binary files differ
diff --git a/doc/integration/img/github_app.png b/doc/integration/img/github_app.png
index d890345ced9..de31242679a 100644
--- a/doc/integration/img/github_app.png
+++ b/doc/integration/img/github_app.png
Binary files differ
diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png
index 3f9391a821b..065316fd3c7 100644
--- a/doc/integration/img/gitlab_app.png
+++ b/doc/integration/img/gitlab_app.png
Binary files differ
diff --git a/doc/integration/img/gmail_action_buttons_for_gitlab.png b/doc/integration/img/gmail_action_buttons_for_gitlab.png
index b08f54d137b..a6704139091 100644
--- a/doc/integration/img/gmail_action_buttons_for_gitlab.png
+++ b/doc/integration/img/gmail_action_buttons_for_gitlab.png
Binary files differ
diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png
index 5a62ad35009..08f7f714553 100644
--- a/doc/integration/img/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
index a2d8e14c120..fc5f7596fcc 100644
--- a/doc/integration/img/oauth_provider_admin_application.png
+++ 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
index 3a676b22393..606ab3e3467 100644
--- a/doc/integration/img/oauth_provider_application_form.png
+++ 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
index 6d68df001af..cbedcef8376 100644
--- a/doc/integration/img/oauth_provider_application_id_secret.png
+++ 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
index efc3b807d71..6a2ea09073c 100644
--- a/doc/integration/img/oauth_provider_authorized_application.png
+++ 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
index 45ad8a6d468..0c7b095a2dd 100644
--- a/doc/integration/img/oauth_provider_user_wide_applications.png
+++ b/doc/integration/img/oauth_provider_user_wide_applications.png
Binary files differ
diff --git a/doc/integration/img/twitter_app_api_keys.png b/doc/integration/img/twitter_app_api_keys.png
index 1076337172a..15b29ac7d16 100644
--- a/doc/integration/img/twitter_app_api_keys.png
+++ b/doc/integration/img/twitter_app_api_keys.png
Binary files differ
diff --git a/doc/integration/img/twitter_app_details.png b/doc/integration/img/twitter_app_details.png
index b95e8af8a74..323112a88bb 100644
--- a/doc/integration/img/twitter_app_details.png
+++ b/doc/integration/img/twitter_app_details.png
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index cab329c0dec..46b260e7033 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -11,6 +11,7 @@ of the configured mechanisms.
- [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)
+- [Enable or disable Sign In with an OmniAuth provider without disabling import sources](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources)
## Supported Providers
@@ -126,9 +127,15 @@ The chosen OmniAuth provider is now active and can be used to sign in to GitLab
This setting was introduced with version 8.7 of GitLab
You can define which OmniAuth providers you want to be `external` so that all users
-creating accounts via these providers will not be able to have access to internal
-projects. You will need to use the full name of the provider, like `google_oauth2`
-for Google. Refer to the examples for the full names of the supported providers.
+**creating accounts, or logging in via these providers** will not be able to have
+access to internal projects. You will need to use the full name of the provider,
+like `google_oauth2` for Google. Refer to the examples for the full names of the
+supported providers.
+
+>**Note:**
+If you decide to remove an OmniAuth provider from the external providers list
+you will need to manually update the users that use this method to login, if you
+want their accounts to be upgraded to full internal accounts.
**For Omnibus installations**
@@ -191,3 +198,17 @@ experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/w
While we can't officially support every possible authentication mechanism out there,
we'd like to at least help those with specific needs.
+
+## Enable or disable Sign In with an OmniAuth provider without disabling import sources
+
+>**Note:**
+This setting was introduced with version 8.8 of GitLab.
+
+Administrators are able to enable or disable Sign In via some OmniAuth providers.
+
+>**Note:**
+By default Sign In is enabled via all the OAuth Providers that have been configured in `config/gitlab.yml`.
+
+In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable.
+
+![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png)
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index b6b2d4e5e88..5210ce0de9a 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -2,7 +2,7 @@
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 omnibus-gitlab 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 this is difficult to configure using the bundled NIGNX provided in the 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/intro/README.md b/doc/intro/README.md
index ab298d3808e..1850031eb26 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -12,7 +12,7 @@ Create projects and groups.
Create issues, labels, milestones, cast your vote, and review issues.
- [Create a new issue](../gitlab-basics/create-issue.md)
-- [Assign labels to issues](../workflow/labels.md)
+- [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../workflow/milestones.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
@@ -39,4 +39,4 @@ Install and update your GitLab installation.
- [Install GitLab](https://about.gitlab.com/installation/)
- [Update GitLab](https://about.gitlab.com/update/)
-- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html)
+- [Explore Omnibus GitLab configuration options](http://docs.gitlab.com/omnibus/settings/configuration.html)
diff --git a/doc/logs/logs.md b/doc/logs/logs.md
index 27937e51764..a2eca62d691 100644
--- a/doc/logs/logs.md
+++ b/doc/logs/logs.md
@@ -1,92 +1 @@
-## Log system
-GitLab has advanced log system so everything is logging and you can analize your instance using various system log files.
-In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html)
-
-System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files.
-
-#### production.log
-This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/log/production.log` for installations from the source.
-
-This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took.
-This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug.
-
-```
-Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
-Processing by Projects::TreeController#show as HTML
- Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
-
- ... [CUT OUT]
-
- amespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
- CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
- CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
-  (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
- Rendered layouts/nav/_project.html.haml (28.0ms)
- Rendered layouts/_collapse_button.html.haml (0.2ms)
- Rendered layouts/_flash.html.haml (0.1ms)
- Rendered layouts/_page.html.haml (32.9ms)
-Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
-```
-In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController.
-
-#### application.log
-This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/log/application.log` for installations from the source.
-
-This log file helps you discover events happening in your instance such as user creation, project removing and so on.
-
-```
-October 06, 2014 11:56: User "Administrator" (admin@example.com) was created
-October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore"
-October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce"
-October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed
-October 07, 2014 11:25: Project "project133" was removed
-```
-#### githost.log
-This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/log/githost.log` for installations from the source.
-
-The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only.
-```
-December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict
-
-error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
-```
-
-#### sidekiq.log
-This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/log/sidekiq.log` for installations from the source.
-
-GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file.
-```
-2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read'
-2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
-```
-
-#### gitlab-shell.log
-This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from the source.
-
-gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories.
-
-```
-I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>.
-I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and simlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
-```
-
-#### unicorn_stderr.log
-This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source.
-
-Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time.
-
-```
-I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list
-I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12
-I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13
-I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready
-I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092
-I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready
-I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094
-I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready
-W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #<Unicorn::HttpServer:0x0000000208f618>: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes)
-W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1)
-I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1
-I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
-I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
-```
+This document was moved to [administration/logs.md](../administration/logs.md).
diff --git a/doc/markdown/img/logo.png b/doc/markdown/img/logo.png
index 7da5f23ed9b..05c8b0d0ccf 100644
--- a/doc/markdown/img/logo.png
+++ b/doc/markdown/img/logo.png
Binary files differ
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 4f199b6af6f..236eb7b12c4 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -8,6 +8,7 @@
* [Multiple underscores in words](#multiple-underscores-in-words)
* [URL auto-linking](#url-auto-linking)
* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
+* [Inline Diff](#inline-diff)
* [Emoji](#emoji)
* [Special GitLab references](#special-gitlab-references)
* [Task lists](#task-lists)
@@ -153,6 +154,19 @@ s = "There is no highlighting for this."
But let's throw in a <b>tag</b>.
```
+## Inline Diff
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags cannot be mixed as such:
+
+- {+ additions +]
+- [+ additions +}
+- {- deletions -]
+- [- deletions -}
+
## Emoji
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
@@ -185,20 +199,23 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
-| input | references |
-|:-----------------------|:---------------------------|
-| `@user_name` | specific user |
-| `@group_name` | specific group |
-| `@all` | entire team |
-| `#123` | issue |
-| `!123` | merge request |
-| `$123` | snippet |
-| `~123` | label by ID |
-| `~bug` | one-word label by name |
-| `~"feature request"` | multi-word label by name |
-| `9ba12248` | specific commit |
-| `9ba12248...b19a04f5` | commit range comparison |
-| `[README](doc/README)` | repository file references |
+| input | references |
+|:-----------------------|:--------------------------- |
+| `@user_name` | specific user |
+| `@group_name` | specific group |
+| `@all` | entire team |
+| `#123` | issue |
+| `!123` | merge request |
+| `$123` | snippet |
+| `~123` | label by ID |
+| `~bug` | one-word label by name |
+| `~"feature request"` | multi-word label by name |
+| `%123` | milestone by ID |
+| `%v1.23` | one-word milestone by name |
+| `%"release candidate"` | multi-word milestone by name |
+| `9ba12248` | specific commit |
+| `9ba12248...b19a04f5` | commit range comparison |
+| `[README](doc/README)` | repository file references |
GFM also recognizes certain cross-project references:
@@ -206,6 +223,7 @@ GFM also recognizes certain cross-project references:
|:----------------------------------------|:------------------------|
| `namespace/project#123` | issue |
| `namespace/project!123` | merge request |
+| `namespace/project%123` | milestone |
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
@@ -402,7 +420,7 @@ There are two ways to create links, inline-style and reference-style.
[I'm a reference-style link][Arbitrary case-insensitive reference text]
-[I'm a relative reference to a repository file](LICENSE)
+[I'm a relative reference to a repository file](LICENSE)[^1]
[You can use numbers for reference-style link definitions][1]
@@ -594,3 +612,4 @@ By including colons in the header row, you can align the text within that column
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
+[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 5ec0a2069b5..8f9ef054949 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -355,7 +355,7 @@ sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
```
#### Problems when importing CI database to GitLab
-If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences:
+If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences:
```
ALTER SEQUENCE
ERROR: relation "ci_builds_id_seq" does not exist
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
new file mode 100644
index 00000000000..0d17799372f
--- /dev/null
+++ b/doc/monitoring/health_check.md
@@ -0,0 +1,66 @@
+# Health Check
+
+>**Note:** This feature was [introduced][ce-3888] in GitLab 8.8.
+
+GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
+endpoint. The health check reports on the overall system status based on the status of
+the database connection, the state of the database migrations, and the ability to write
+and access the cache. This endpoint can be provided to uptime monitoring services like
+[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
+
+## Access Token
+
+An access token needs to be provided while accessing the health check endpoint. The current
+accepted token can be found on the `admin/health_check` page of your GitLab instance.
+
+![access token](img/health_check_token.png)
+
+The access token can be passed as a URL parameter:
+
+```
+https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
+```
+
+or as an HTTP header:
+
+```bash
+curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+## Using the Endpoint
+
+Once you have the access token, health information can be retrieved as plain text, JSON,
+or XML using the `health_check` endpoint:
+
+- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN`
+
+You can also ask for the status of specific services:
+
+- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN`
+- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN`
+
+For example, the JSON output of the following health check:
+
+```bash
+curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+```
+
+would be like:
+
+```
+{"healthy":true,"message":"success"}
+```
+
+## Status
+
+On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
+will return a valid successful HTTP status code, and a `success` message. Ideally your
+uptime monitoring should look for the success message.
+
+[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
+[pingdom]: https://www.pingdom.com
+[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
+[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
diff --git a/doc/monitoring/img/health_check_token.png b/doc/monitoring/img/health_check_token.png
new file mode 100644
index 00000000000..2d7c82a65a8
--- /dev/null
+++ b/doc/monitoring/img/health_check_token.png
Binary files differ
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 168bd85c26a..7947b0fedc4 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -44,70 +44,32 @@ on a separate server)
## Apply retention policies and create continuous queries
-If you intend to import the GitLab provided Grafana dashboards, you will need
-to copy and run a set of queries against InfluxDB to create the needed data
-sets.
+If you intend to import the GitLab provided Grafana dashboards, you will need to
+set up the right retention policies and continuous queries. The easiest way of
+doing this is by using the [influxdb-management](https://gitlab.com/gitlab-org/influxdb-management)
+repository.
-On the InfluxDB server, run the following command, substituting your InfluxDB
-user and password:
+To use this repository you must first clone it:
-```bash
-influxdb --username admin -password super_secret
+```
+git clone https://gitlab.com/gitlab-org/influxdb-management.git
+cd influxdb-management
```
-This will drop you in to an InfluxDB interactive session. Copy the entire
-contents below and paste it in to the interactive session:
+Next you must install the required dependencies:
```
-CREATE RETENTION POLICY default ON gitlab DURATION 1h REPLICATION 1 DEFAULT
-CREATE RETENTION POLICY downsampled ON gitlab DURATION 7d REPLICATION 1
-CREATE CONTINUOUS QUERY grape_git_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.grape_git_timings_per_action FROM gitlab."default".rails_method_calls WHERE (action !~ /.+/ OR action =~ /^Grape#/) AND method =~ /^(Rugged|Gitlab::Git)/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY grape_markdown_render_timings_overall ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.grape_markdown_render_timings_overall FROM gitlab."default".rails_transactions WHERE (action !~ /.+/ OR action =~ /^Grape#/) AND (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY grape_markdown_render_timings_per_action ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.grape_markdown_render_timings_per_action FROM gitlab."default".rails_transactions WHERE (action !~ /.+/ OR action =~ /^Grape#/) AND (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY grape_markdown_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.grape_markdown_timings_overall FROM gitlab."default".rails_method_calls WHERE (action !~ /.+/ OR action =~ /^Grape#/) AND method =~ /^Banzai/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY grape_method_call_timings_per_action_and_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.grape_method_call_timings_per_action_and_method FROM gitlab."default".rails_method_calls WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m), method, action END;
-CREATE CONTINUOUS QUERY grape_method_call_timings_per_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.grape_method_call_timings_per_method FROM gitlab."default".rails_method_calls WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m), method END;
-CREATE CONTINUOUS QUERY grape_transaction_counts_overall ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.grape_transaction_counts_overall FROM gitlab."default".rails_transactions WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY grape_transaction_counts_per_action ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.grape_transaction_counts_per_action FROM gitlab."default".rails_transactions WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY grape_transaction_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.grape_transaction_timings_overall FROM gitlab."default".rails_transactions WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY grape_transaction_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.grape_transaction_timings_per_action FROM gitlab."default".rails_transactions WHERE action !~ /.+/ OR action =~ /^Grape#/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY rails_file_descriptor_counts ON gitlab BEGIN SELECT sum(value) AS count INTO gitlab.downsampled.rails_file_descriptor_counts FROM gitlab."default".rails_file_descriptors GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_gc_counts ON gitlab BEGIN SELECT sum(count) AS total, sum(minor_gc_count) AS minor, sum(major_gc_count) AS major INTO gitlab.downsampled.rails_gc_counts FROM gitlab."default".rails_gc_statistics GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_gc_timings ON gitlab BEGIN SELECT mean(total_time) AS duration_mean, percentile(total_time, 95) AS duration_95th, percentile(total_time, 99) AS duration_99th INTO gitlab.downsampled.rails_gc_timings FROM gitlab."default".rails_gc_statistics GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_git_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.rails_git_timings_per_action FROM gitlab."default".rails_method_calls WHERE (action =~ /.+/ AND action !~ /^Grape#/) AND method =~ /^(Rugged|Gitlab::Git)/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY rails_markdown_render_timings_overall ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.rails_markdown_render_timings_overall FROM gitlab."default".rails_transactions WHERE (action =~ /.+/ AND action !~ /^Grape#/) AND (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_markdown_render_timings_per_action ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.rails_markdown_render_timings_per_action FROM gitlab."default".rails_transactions WHERE (action =~ /.+/ AND action !~ /^Grape#/) AND (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY rails_markdown_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.rails_markdown_timings_overall FROM gitlab."default".rails_method_calls WHERE (action =~ /.+/ AND action !~ /^Grape#/) AND method =~ /^Banzai/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_memory_usage_overall ON gitlab BEGIN SELECT mean(value) AS memory_mean, percentile(value, 95) AS memory_95th, percentile(value, 99) AS memory_99th INTO gitlab.downsampled.rails_memory_usage_overall FROM gitlab."default".rails_memory_usage GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_method_call_timings_per_action_and_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.rails_method_call_timings_per_action_and_method FROM gitlab."default".rails_method_calls WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m), method, action END;
-CREATE CONTINUOUS QUERY rails_method_call_timings_per_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.rails_method_call_timings_per_method FROM gitlab."default".rails_method_calls WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m), method END;
-CREATE CONTINUOUS QUERY rails_object_counts_overall ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.downsampled.rails_object_counts_overall FROM gitlab."default".rails_object_counts GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_object_counts_per_type ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.downsampled.rails_object_counts_per_type FROM gitlab."default".rails_object_counts GROUP BY time(1m), type END;
-CREATE CONTINUOUS QUERY rails_transaction_counts_overall ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.rails_transaction_counts_overall FROM gitlab."default".rails_transactions WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_transaction_counts_per_action ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.rails_transaction_counts_per_action FROM gitlab."default".rails_transactions WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY rails_transaction_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.rails_transaction_timings_overall FROM gitlab."default".rails_transactions WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY rails_transaction_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.rails_transaction_timings_per_action FROM gitlab."default".rails_transactions WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY rails_view_timings_per_action_and_view ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.rails_view_timings_per_action_and_view FROM gitlab."default".rails_views WHERE action =~ /.+/ AND action !~ /^Grape#/ GROUP BY time(1m), action, view END;
-CREATE CONTINUOUS QUERY sidekiq_file_descriptor_counts ON gitlab BEGIN SELECT sum(value) AS count INTO gitlab.downsampled.sidekiq_file_descriptor_counts FROM gitlab."default".sidekiq_file_descriptors GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_gc_counts ON gitlab BEGIN SELECT sum(count) AS total, sum(minor_gc_count) AS minor, sum(major_gc_count) AS major INTO gitlab.downsampled.sidekiq_gc_counts FROM gitlab."default".sidekiq_gc_statistics GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_gc_timings ON gitlab BEGIN SELECT mean(total_time) AS duration_mean, percentile(total_time, 95) AS duration_95th, percentile(total_time, 99) AS duration_99th INTO gitlab.downsampled.sidekiq_gc_timings FROM gitlab."default".sidekiq_gc_statistics GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_git_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.sidekiq_git_timings_per_action FROM gitlab."default".sidekiq_method_calls WHERE method =~ /^(Rugged|Gitlab::Git)/ GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY sidekiq_markdown_render_timings_overall ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.sidekiq_markdown_render_timings_overall FROM gitlab."default".sidekiq_transactions WHERE (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_markdown_render_timings_per_action ON gitlab BEGIN SELECT mean(banzai_cached_render_real_time) AS cached_real_mean, percentile(banzai_cached_render_real_time, 95) AS cached_real_95th, percentile(banzai_cached_render_real_time, 99) AS cached_real_99th, mean(banzai_cached_render_cpu_time) AS cached_cpu_mean, percentile(banzai_cached_render_cpu_time, 95) AS cached_cpu_95th, percentile(banzai_cached_render_cpu_time, 99) AS cached_cpu_99th, sum(banzai_cached_render_call_count) AS cached_call_count, mean(banzai_cacheless_render_real_time) AS cacheless_real_mean, percentile(banzai_cacheless_render_real_time, 95) AS cacheless_real_95th, percentile(banzai_cacheless_render_real_time, 99) AS cacheless_real_99th, mean(banzai_cacheless_render_cpu_time) AS cacheless_cpu_mean, percentile(banzai_cacheless_render_cpu_time, 95) AS cacheless_cpu_95th, percentile(banzai_cacheless_render_cpu_time, 99) AS cacheless_cpu_99th, sum(banzai_cacheless_render_call_count) AS cacheless_call_count INTO gitlab.downsampled.sidekiq_markdown_render_timings_per_action FROM gitlab."default".sidekiq_transactions WHERE (banzai_cached_render_call_count > 0 OR banzai_cacheless_render_call_count > 0) GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY sidekiq_markdown_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.sidekiq_markdown_timings_overall FROM gitlab."default".sidekiq_method_calls WHERE method =~ /^Banzai/ GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_memory_usage_overall ON gitlab BEGIN SELECT mean(value) AS memory_mean, percentile(value, 95) AS memory_95th, percentile(value, 99) AS memory_99th INTO gitlab.downsampled.sidekiq_memory_usage_overall FROM gitlab."default".sidekiq_memory_usage GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_action_and_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.sidekiq_method_call_timings_per_action_and_method FROM gitlab."default".sidekiq_method_calls GROUP BY time(1m), method, action END;
-CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.sidekiq_method_call_timings_per_method FROM gitlab."default".sidekiq_method_calls GROUP BY time(1m), method END;
-CREATE CONTINUOUS QUERY sidekiq_object_counts_overall ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.downsampled.sidekiq_object_counts_overall FROM gitlab."default".sidekiq_object_counts GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_object_counts_per_type ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.downsampled.sidekiq_object_counts_per_type FROM gitlab."default".sidekiq_object_counts GROUP BY time(1m), type END;
-CREATE CONTINUOUS QUERY sidekiq_transaction_counts_overall ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.sidekiq_transaction_counts_overall FROM gitlab."default".sidekiq_transactions GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_transaction_counts_per_action ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.sidekiq_transaction_counts_per_action FROM gitlab."default".sidekiq_transactions GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY sidekiq_transaction_timings_overall ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.sidekiq_transaction_timings_overall FROM gitlab."default".sidekiq_transactions GROUP BY time(1m) END;
-CREATE CONTINUOUS QUERY sidekiq_transaction_timings_per_action ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(sql_duration, 95) AS sql_duration_95th, percentile(sql_duration, 99) AS sql_duration_99th, mean(view_duration) AS view_duration_mean, percentile(view_duration, 95) AS view_duration_95th, percentile(view_duration, 99) AS view_duration_99th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_duration) AS cache_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(method_duration) AS method_duration_mean, percentile(method_duration, 99) AS method_duration_99th, percentile(method_duration, 95) AS method_duration_95th INTO gitlab.downsampled.sidekiq_transaction_timings_per_action FROM gitlab."default".sidekiq_transactions GROUP BY time(1m), action END;
-CREATE CONTINUOUS QUERY sidekiq_view_timings_per_action_and_view ON gitlab BEGIN SELECT mean("duration") AS duration_mean, percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th INTO gitlab.downsampled.sidekiq_view_timings_per_action_and_view FROM gitlab."default".sidekiq_views GROUP BY time(1m), action, view END;
-CREATE CONTINUOUS QUERY web_transaction_counts_overall ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.downsampled.web_transaction_counts_overall FROM gitlab."default".rails_transactions GROUP BY time(1m) END;
+gem install bundler
+bundle install
```
+Now you must configure the repository by first copying `.env.example` to `.env`
+and then editing the `.env` file to contain the correct InfluxDB settings. Once
+configured you can simply run `bundle exec rake` and the InfluxDB database will
+be configured for you.
+
+For more information see the [influxdb-management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
+
## Import Dashboards
You can now import a set of default dashboards that will give you a good
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
index b4448c7a09f..7e34fad71ce 100644
--- a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
+++ b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
index 5a2d3c0937a..f97624365c7 100644
--- a/doc/monitoring/performance/img/grafana_dashboard_import.png
+++ b/doc/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
index 7e2e111f570..7d50e4c88c2 100644
--- a/doc/monitoring/performance/img/grafana_data_source_configuration.png
+++ b/doc/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
index 11e27571e64..aa39a53acae 100644
--- a/doc/monitoring/performance/img/grafana_data_source_empty.png
+++ b/doc/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
index 3d4265bee8e..c740e33cd1c 100644
--- a/doc/monitoring/performance/img/grafana_save_icon.png
+++ b/doc/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
index 14d82b6ac98..e6ed45a0386 100644
--- a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
+++ b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md
index 39086b7a251..54adb99386a 100644
--- a/doc/operations/moving_repositories.md
+++ b/doc/operations/moving_repositories.md
@@ -134,7 +134,7 @@ sudo -u git sh -c '
cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\
/usr/bin/env JOBS=10 \
/opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- /var/opt/gitlab/transfer-logs/succes-$(date +%s).log \
+ /var/opt/gitlab/transfer-logs/success-$(date +%s).log \
/var/opt/gitlab/git-data/repositories \
/mnt/gitlab/repositories
'
@@ -145,7 +145,7 @@ sudo -u git -H sh -c '
cat /home/git/transfer-logs/* | sort | uniq -u |\
/usr/bin/env JOBS=10 \
bin/parallel-rsync-repos \
- /home/git/transfer-logs/succes-$(date +%s).log \
+ /home/git/transfer-logs/success-$(date +%s).log \
/home/git/repositories \
/mnt/gitlab/repositories
`
@@ -164,7 +164,7 @@ sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
sudo -u git \
/usr/bin/env JOBS=10 \
/opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \
- succes-$(date +%s).log \
+ success-$(date +%s).log \
/var/opt/gitlab/git-data/repositories \
/mnt/gitlab/repositories
@@ -174,7 +174,7 @@ sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\
sudo -u git -H \
/usr/bin/env JOBS=10 \
bin/parallel-rsync-repos \
- succes-$(date +%s).log \
+ success-$(date +%s).log \
/home/git/repositories \
/mnt/gitlab/repositories
```
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 6219693b8a8..44f3f6d3b12 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -27,6 +27,8 @@ documentation](../workflow/add-user/add-user.md).
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
+| See a container registry | | ✓ | ✓ | ✓ | ✓ |
+| See environments | | ✓ | ✓ | ✓ | ✓ |
| Manage merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -37,6 +39,9 @@ documentation](../workflow/add-user/add-user.md).
| Write a wiki | | | ✓ | ✓ | ✓ |
| Cancel and retry builds | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ |
+| Update a container registry | | | ✓ | ✓ | ✓ |
+| Remove a container registry image | | | ✓ | ✓ | ✓ |
+| Create new environments | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
@@ -49,6 +54,7 @@ documentation](../workflow/add-user/add-user.md).
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Delete environments | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
@@ -93,3 +99,6 @@ 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.
+
+By default new users are not set as external users. This behavior can be changed
+by an administrator under **Admin > Application Settings**. \ No newline at end of file
diff --git a/doc/profile/2fa.png b/doc/profile/2fa.png
index bbf415210d5..bb464efa685 100644
--- a/doc/profile/2fa.png
+++ b/doc/profile/2fa.png
Binary files differ
diff --git a/doc/profile/2fa_auth.png b/doc/profile/2fa_auth.png
index 4a4fbe68984..0caaed10805 100644
--- a/doc/profile/2fa_auth.png
+++ b/doc/profile/2fa_auth.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png
new file mode 100644
index 00000000000..b9138ff60db
--- /dev/null
+++ b/doc/profile/2fa_u2f_authenticate.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_register.png b/doc/profile/2fa_u2f_register.png
new file mode 100644
index 00000000000..15b3683ef73
--- /dev/null
+++ b/doc/profile/2fa_u2f_register.png
Binary files differ
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index a0e23c1586c..82505b13401 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -8,12 +8,27 @@ your phone.
By enabling 2FA, the only way someone other than you can log into your account
is to know your username and password *and* have access to your phone.
-#### Note
+> **Note:**
When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
lose your codes for GitLab.com, we can't disable or recover them.
+In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
+the second factor of authentication. Once enabled, in addition to supplying your username and
+password to login, you'll be prompted to activate your U2F device (usually by pressing
+a button on it), and it will perform secure authentication on your behalf.
+
+> **Note:** Support for U2F devices was added in version 8.8
+
+The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
+that you set up both methods of two-factor authentication, so you can still access your account
+from other browsers.
+
+> **Note:** GitLab officially only supports [Yubikey] U2F devices.
+
## Enabling 2FA
+### Enable 2FA via mobile application
+
**In GitLab:**
1. Log in to your GitLab account.
@@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them.
1. Click **Submit**.
If the pin you entered was correct, you'll see a message indicating that
-Two-factor Authentication has been enabled, and you'll be presented with a list
+Two-Factor Authentication has been enabled, and you'll be presented with a list
of recovery codes.
+### Enable 2FA via U2F device
+
+**In GitLab:**
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Enable Two-Factor Authentication**.
+1. Plug in your U2F device.
+1. Click on **Setup New U2F Device**.
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device was successfully set up.
+Click on **Register U2F Device** to complete the process.
+
+![Two-Factor U2F Setup](2fa_u2f_register.png)
+
## Recovery Codes
Should you ever lose access to your phone, you can use one of the ten provided
@@ -51,21 +83,39 @@ account.
If you lose the recovery codes or just want to generate new ones, you can do so
from the **Profile Settings** > **Account** page where you first enabled 2FA.
+> **Note:** Recovery codes are not generated for U2F devices.
+
## Logging in with 2FA Enabled
Logging in with 2FA enabled is only slightly different than a normal login.
Enter your username and password credentials as you normally would, and you'll
-be presented with a second prompt for an authentication code. Enter the pin from
-your phone's application or a recovery code to log in.
+be presented with a second prompt, depending on which type of 2FA you've enabled.
+
+### Log in via mobile application
+
+Enter the pin from your phone's application or a recovery code to log in.
-![Two-factor authentication on sign in](2fa_auth.png)
+![Two-Factor Authentication on sign in via OTP](2fa_auth.png)
+
+### Log in via U2F device
+
+1. Click **Login via U2F Device**
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device responded to the authentication request.
+Click on **Authenticate via U2F Device** to complete the process.
+
+![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png)
## Disabling 2FA
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
-1. Click **Disable Two-factor Authentication**.
+1. Click **Disable**, under **Two-Factor Authentication**.
+
+This will clear all your two-factor authentication registrations, including mobile
+applications and U2F devices.
## Note to GitLab administrators
@@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://fedorahosted.org/freeotp/
+[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md
new file mode 100644
index 00000000000..215ed6fe9cc
--- /dev/null
+++ b/doc/project_services/bugzilla.md
@@ -0,0 +1,17 @@
+# Bugzilla Service
+
+Go to your project's **Settings > Services > Bugzilla** 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 Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+| `issues_url` | The URL to the issue in Bugzilla 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 Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+
+Once you have configured and enabled Bugzilla:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Bugzilla product page
+- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
new file mode 100644
index 00000000000..2f9f36f962e
--- /dev/null
+++ b/doc/project_services/emails_on_push.md
@@ -0,0 +1,17 @@
+## Enabling emails on push
+
+To receive email notifications for every change that is pushed to the project, visit
+your project's **Settings > Services > Emails on push** and activate the service.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
index e604dd73ffa..88943dc410e 100644
--- a/doc/project_services/img/builds_emails_service.png
+++ b/doc/project_services/img/builds_emails_service.png
Binary files differ
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png
new file mode 100644
index 00000000000..cd6f79ad1eb
--- /dev/null
+++ b/doc/project_services/img/emails_on_push_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
index 85e54861b3e..aec472b9118 100644
--- a/doc/project_services/img/jira_add_gitlab_commit_message.png
+++ 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
index e4576433889..0ba737bda9a 100644
--- a/doc/project_services/img/jira_add_user_to_group.png
+++ 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
index edaa1326058..0609060cb05 100644
--- a/doc/project_services/img/jira_create_new_group.png
+++ 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
index 9e518ad7843..53d77b17df0 100644
--- a/doc/project_services/img/jira_create_new_group_name.png
+++ 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
index 57e433dd818..9eaa444ed25 100644
--- a/doc/project_services/img/jira_create_new_user.png
+++ 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
index 47716ca6d0e..8d4657427ae 100644
--- a/doc/project_services/img/jira_group_access.png
+++ 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
index cabec1ae137..acdd83702d3 100644
--- a/doc/project_services/img/jira_issue_closed.png
+++ b/doc/project_services/img/jira_issue_closed.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png
index 15739a22dc7..1a2d9f04a6c 100644
--- a/doc/project_services/img/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
index 28e17be3a84..0703081d77b 100644
--- a/doc/project_services/img/jira_issues_workflow.png
+++ 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
index 1e78daf105f..47785e3ba27 100644
--- a/doc/project_services/img/jira_merge_request_close.png
+++ b/doc/project_services/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png
index 5986fdb63fb..e785ec6140d 100644
--- a/doc/project_services/img/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
index 0149181dc86..fb270d85e3c 100644
--- 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
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png
index 1f6628c4371..13aefce6f84 100644
--- a/doc/project_services/img/jira_service.png
+++ b/doc/project_services/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png
index 67dfc6144c4..eed69e80d2c 100644
--- a/doc/project_services/img/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
index c225daa81e1..a5b49c501ba 100644
--- a/doc/project_services/img/jira_service_page.png
+++ 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
index e935d9362aa..77630d39d39 100644
--- a/doc/project_services/img/jira_submit_gitlab_merge_request.png
+++ 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
index 2745916972c..5f002b59bac 100644
--- a/doc/project_services/img/jira_user_management_link.png
+++ b/doc/project_services/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png
index 8635a32eb68..937a50a77d9 100644
--- a/doc/project_services/img/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
index d14e526ad33..e9d8c0d2da8 100644
--- a/doc/project_services/img/redmine_configuration.png
+++ 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
index 384d057fc8e..77c2b98e5d0 100644
--- a/doc/project_services/img/services_templates_redmine_example.png
+++ b/doc/project_services/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index a5af620d9be..e15d5db3253 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -30,10 +30,11 @@ further configuration instructions and details. Contributions are welcome.
| [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 |
+| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
| 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 |
+| [Emails on push](emails_on_push.md) | 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 |
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 17bb75ececd..9a5c5a5c92a 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -17,7 +17,7 @@ Public projects can be cloned **without any** authentication.
They will also be listed on the public access directory (`/public`).
-**Any logged in user** will have [Guest](../permissions/permissions)
+**Any logged in user** will have [Guest](../permissions/permissions.md)
permissions on the repository.
### Internal projects
@@ -27,8 +27,8 @@ Internal projects can be cloned by any logged in user.
They will also be listed on the public access directory (`/public`) for logged
in users.
-Any logged in user will have [Guest](../permissions/permissions) permissions on
-the repository.
+Any logged in user will have [Guest](../permissions/permissions.md) permissions
+on the repository.
### How to change project visibility
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 6be954ad68b..a49c43b8ef2 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -8,4 +8,4 @@
- [User management](user_management.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
+- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png
index 03e50df1d76..42084717ebe 100644
--- a/doc/raketasks/backup_hrz.png
+++ b/doc/raketasks/backup_hrz.png
Binary files differ
diff --git a/doc/raketasks/check_repos_output.png b/doc/raketasks/check_repos_output.png
index 916b1685101..1f632566b00 100644
--- a/doc/raketasks/check_repos_output.png
+++ b/doc/raketasks/check_repos_output.png
Binary files differ
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 8a38937062e..2b305cb5c99 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -14,7 +14,8 @@
- For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed
it in the `/etc/gitlab/gitlab.rb` file.
- For installations from source, it is usually located at: `/home/git/repositories` or you can see where
-your repositories are located by looking at `config/gitlab.yml` under the `gitlab_shell => repos_path` entry.
+your repositories are located by looking at `config/gitlab.yml` under the `repositories => storages` entries
+(you'll usually use the `default` storage path to start).
New folder needs to have git user ownership and read/write/execute access for git user and its group:
diff --git a/doc/security/README.md b/doc/security/README.md
index 4cd0fdd4094..38706e48ec5 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -8,3 +8,4 @@
- [User File Uploads](user_file_uploads.md)
- [How we manage the CRIME vulnerability](crime_vulnerability.md)
- [Enforce Two-factor authentication](two_factor_authentication.md)
+- [Send email confirmation on sign-up](user_email_confirmation.md)
diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png
index aa51ce030bb..6af5feabb13 100644
--- a/doc/security/img/two_factor_authentication_settings.png
+++ b/doc/security/img/two_factor_authentication_settings.png
Binary files differ
diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md
new file mode 100644
index 00000000000..4293944ae8b
--- /dev/null
+++ b/doc/security/user_email_confirmation.md
@@ -0,0 +1,7 @@
+# User email confirmation at sign-up
+
+Gitlab admin can enable email confirmation on sign-up, if you want to confirm all
+user emails before they are able to sign-in.
+
+In the Admin area under **Settings** (`/admin/application_settings`), go to section
+**Sign-in Restrictions** and look for **Send confirmation email on sign-up** option.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index a1198e5878f..d6a0979f6ec 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -17,10 +17,6 @@ cat ~/.ssh/id_rsa.pub
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
-the password you choose here can't be altered or retrieved.
-
To generate a new SSH key, use the following command:
```bash
ssh-keygen -t rsa -C "$your_email"
@@ -30,6 +26,12 @@ 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.
+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.
+
+If you want to change the password of your key later, you can use the following
+command: `ssh-keygen -p <keyname>`
+
Use the command below to show your public key:
**Windows Command Line:**
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index 4827ef9501a..fb70eaacbc9 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -13,6 +13,10 @@
git fetch origin
git checkout v3.0.3
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
# Install libs
sudo -u gitlab bundle install --without development test postgres
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index f4a997a8c5e..ce46b57c09a 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -13,6 +13,11 @@
sudo -u gitlab -H git fetch origin
sudo -u gitlab -H git checkout v3.0.3
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
+
# Install gems
sudo -u gitlab -H bundle install --without development test postgres
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index a30485c42f7..6ac83f3b60d 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -25,6 +25,11 @@ sudo -u gitlab -H git checkout v3.1.0
# Install new charlock_holmes
sudo gem install charlock_holmes --version '0.6.9'
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
+
# Install gems for MySQL
sudo -u gitlab -H bundle install --without development test postgres sqlite
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index f1ef4df4744..df53ed6de83 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -26,6 +26,11 @@ I wrote a bash script which will do it automatically for you. Just make sure all
sudo -u gitlab -H git fetch
sudo -u gitlab -H git checkout 4-0-stable
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
+
# Install gems for MySQL
sudo -u gitlab -H bundle install --without development test postgres
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index d89d5235917..c163bfd348d 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -22,6 +22,11 @@ cd /home/gitlab/gitlab/
sudo -u gitlab -H git fetch
sudo -u gitlab -H git checkout 4-1-stable
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
+
# Install gems for MySQL
sudo -u gitlab -H bundle install --without development test postgres
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 6fe4412ff90..97367c5f347 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -17,7 +17,15 @@ sudo -u gitlab -H git fetch
sudo -u gitlab -H git checkout 4-2-stable
-# Install libs
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u gitlab -H vim Gemfile
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u gitlab -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u gitlab -H bundle install --without development test postgres --deployment
# update db
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index f9faf65f952..ee6de51c923 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -85,8 +85,17 @@ sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml
# edit it
sudo -u git -H vim config/gitlab.yml
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
+
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
sudo -u git -H bundle exec rake gitlab:shell:build_missing_projects RAILS_ENV=production
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index 9fbd1f88515..f0fddcf83af 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -42,7 +42,17 @@ cd /home/git/gitlab
sudo rm tmp/sockets/gitlab.socket
sudo -u git -H cp config/puma.rb.example config/puma.rb
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
+
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_merge_requests RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index cf9c4e4f770..625fcc33852 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -40,12 +40,28 @@ sudo -u git -H git checkout v1.4.0
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index 97a98ede070..547d453914c 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -37,12 +37,28 @@ sudo -u git -H git checkout v1.7.9 # Addresses multiple critical security vulner
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index a3fdd92bd2f..c992c69678e 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -137,12 +137,28 @@ sudo apt-get install python-docutils
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_groups RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_global_projects RAILS_ENV=production
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index 27613aeda07..c5254f6fb0c 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -31,12 +31,28 @@ sudo -u git -H git checkout 5-3-stable
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 577b9a585ff..c4a6146dcda 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -35,12 +35,28 @@ sudo -u git -H git checkout v1.7.9 # Addresses multiple critical security vulner
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index d9c6d9bfb91..f0fee634322 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -73,12 +73,28 @@ sudo apt-get install python-docutils
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_groups RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_global_projects RAILS_ENV=production
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index c5eba1c01c4..409faf30902 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -50,13 +50,28 @@ sudo -u git -H git checkout v1.7.9
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake migrate_iids RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index a534528108a..150c7ae1c83 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -45,13 +45,28 @@ sudo apt-get install logrotate
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-#PostgreSQL
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index b08ebde0808..b96dfb8add7 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -40,13 +40,28 @@ The gitlab-shell config changed recently, so check for config file changes and m
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
# PostgreSQL
-sudo -u git -H bundle install --without development test mysql --deployment
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 951d92dfeb5..37028be055f 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -36,13 +36,28 @@ sudo -u git -H git checkout v1.8.0
```bash
cd /home/git/gitlab
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
# PostgreSQL
-sudo -u git -H bundle install --without development test mysql --deployment
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 0dae9a9fe59..982381a4db0 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -46,13 +46,28 @@ sudo -u git -H git checkout v1.8.0
```bash
cd /home/git/gitlab
-# MySQL installations (note: the line below states '--without ... postgres')
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
+# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-# PostgreSQL installations (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index c24e83eb006..bbed2b30215 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -46,12 +46,28 @@ sudo -u git -H git checkout v1.8.0
```bash
cd /home/git/gitlab
-# MySQL installations (note: the line below states '--without ... postgres')
+# The Modernizr gem was yanked from RubyGems. It is required for GitLab >= 2.8.0
+# Edit `Gemfile` and change `gem "modernizr", "2.5.3"` to
+# `gem "modernizr-rails", "2.7.1"``
+sudo -u git -H vim Gemfile
+
+# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-# PostgreSQL installations (note: the line below states '--without ... mysql')
+# PostgreSQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index b4298c93429..8e82942a1a0 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -46,13 +46,23 @@ sudo -u git -H git checkout v1.9.1
```bash
cd /home/git/gitlab
-# MySQL installations (note: the line below states '--without ... postgres')
+# MySQL
+
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test postgres --no-deployment
+
+# Install libs (with deployment this time)
sudo -u git -H bundle install --without development test postgres --deployment
-# PostgreSQL installations (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL
+# Run a bundle install without deployment to generate the new Gemfile
+sudo -u git -H bundle install --without development test mysql --no-deployment
+
+# Install libs (with deployment this time)
+sudo -u git -H bundle install --without development test mysql --deployment
+# Both MySQL and PostgreSQL
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
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 c45fc9340ea..f170a0021b7 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
@@ -147,12 +147,15 @@ sudo -u git -H bundle install --without development test postgres --deployment
# PostgreSQL installations (note: the line below states '--without ... mysql')
sudo -u git -H bundle install --without development test mysql --deployment
-# Run database migrations
-sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+# Run database migrations from 6.0 to 6.1
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production VERSION=20130909132950
# Enable internal issue IDs (introduced in GitLab 6.1)
sudo -u git -H bundle exec rake migrate_iids RAILS_ENV=production
+# Run left 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/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index 4a2c6ea91d2..cb66ef920bb 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -45,7 +45,7 @@ sudo -u git -H git checkout 8-7-stable-ee
```bash
cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --tags
sudo -u git -H git checkout v2.7.2
```
@@ -86,6 +86,14 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
### 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-6-stable:config/gitlab.yml.example origin/8-7-stable:config/gitlab.yml.example
+```
+
#### Git configuration
Disable `git gc --auto` because GitLab runs `git gc` for us already.
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
new file mode 100644
index 00000000000..32906650f6f
--- /dev/null
+++ b/doc/update/8.7-to-8.8.md
@@ -0,0 +1,162 @@
+# From 8.7 to 8.8
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 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-8-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-8-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v2.7.2
+```
+
+### 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 v0.7.1
+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-7-stable:config/gitlab.yml.example origin/8-8-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Disable `git gc --auto` because GitLab runs `git gc` for us already.
+
+```sh
+sudo -u git -H git config --global gc.auto 0
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-7-stable:lib/support/nginx/gitlab-ssl origin/8-8-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-7-stable:lib/support/nginx/gitlab origin/8-8-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-8-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.7)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.6 to 8.7](8.6-to-8.7.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.8-to-8.9.md b/doc/update/8.8-to-8.9.md
new file mode 100644
index 00000000000..f078a2bece5
--- /dev/null
+++ b/doc/update/8.8-to-8.9.md
@@ -0,0 +1,191 @@
+# From 8.8 to 8.9
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 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-9-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-9-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.0.0
+```
+
+### 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 v0.7.5
+sudo -u git -H make
+```
+
+### 6. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+# Login to MySQL
+mysql -u root -p
+
+# Grant the GitLab user the REFERENCES permission on the database
+GRANT REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
+
+# Quit the database session
+mysql> \q
+```
+
+### 7. 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
+
+```
+
+### 8. 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-8-stable:config/gitlab.yml.example origin/8-9-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Disable `git gc --auto` because GitLab runs `git gc` for us already.
+
+```sh
+sudo -u git -H git config --global gc.auto 0
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-8-stable:lib/support/nginx/gitlab-ssl origin/8-9-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-8-stable:lib/support/nginx/gitlab origin/8-9-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-9-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/v8.9.0/config/initializers/smtp_settings.rb.sample#L13
+
+#### 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
+
+### 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.8)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.7 to 8.8](8.7-to-8.8.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.9-to-8.10.md b/doc/update/8.9-to-8.10.md
new file mode 100644
index 00000000000..a51790b0bda
--- /dev/null
+++ b/doc/update/8.9-to-8.10.md
@@ -0,0 +1,191 @@
+# From 8.9 to 8.10
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 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-10-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-10-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.1.0
+```
+
+### 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 v0.7.7
+sudo -u git -H make
+```
+
+### 6. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+# Login to MySQL
+mysql -u root -p
+
+# Grant the GitLab user the REFERENCES permission on the database
+GRANT REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
+
+# Quit the database session
+mysql> \q
+```
+
+### 7. 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
+
+```
+
+### 8. 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-9-stable:config/gitlab.yml.example origin/8-10-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Disable `git gc --auto` because GitLab runs `git gc` for us already.
+
+```sh
+sudo -u git -H git config --global gc.auto 0
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-9-stable:lib/support/nginx/gitlab-ssl origin/8-10-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-9-stable:lib/support/nginx/gitlab origin/8-10-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-10-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/v8.9.0/config/initializers/smtp_settings.rb.sample#L13
+
+#### 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
+
+### 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.9)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.8 to 8.9](8.8-to-8.9.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 a770633c9b8..975d72164b4 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -29,7 +29,7 @@ Based on your installation, choose a section below that fits your needs.
## Omnibus Packages
-- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html)
+- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html)
contains the steps needed to update an Omnibus GitLab package.
## Installation from source
@@ -86,10 +86,10 @@ possible.
information about configuring GitLab to work with a MySQL database.
- [Restoring from backup after a failed upgrade](restore_after_failure.md)
-[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html
+[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html
[source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update
[source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[ee-ce]: ../downgrade_ee_to_ce/README.md
[ce]: https://about.gitlab.com/features/#community
[ee]: https://about.gitlab.com/features/#enterprise
-[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition
+[omni-ce-ee]: http://docs.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index b4283a526f3..60729316cde 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -57,10 +57,10 @@ sudo -u git -H make
cd /home/git/gitlab
# PostgreSQL
-sudo -u git -H bundle install --without development test mysql --with postgres --deployment
+sudo -u git -H bundle install --without development test mysql --deployment
# MySQL
-sudo -u git -H bundle install --without development test postgres --with mysql --deployment
+sudo -u git -H bundle install --without development test postgres --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
diff --git a/doc/user/project/highlighting.md b/doc/user/project/highlighting.md
new file mode 100644
index 00000000000..73a2d176b54
--- /dev/null
+++ b/doc/user/project/highlighting.md
@@ -0,0 +1,31 @@
+[Rouge]: https://rubygems.org/gems/rouge
+
+# Syntax Highlighting
+
+GitLab provides syntax highlighting on all files and snippets through the [Rouge][] rubygem. It will try to guess what language to use based on the file extension, which most of the time is sufficient.
+
+If GitLab is guessing wrong, you can override its choice of language using the `gitlab-language` attribute in `.gitattributes`. For example, if you are working in a Prolog project and using the `.pl` file extension (which would normally be highlighted as Perl), you can add the following to your `.gitattributes` file:
+
+``` conf
+*.pl gitlab-language=prolog
+```
+
+When you check in and push that change, all `*.pl` files in your project will be highlighted as Prolog.
+
+The paths here are simply git's builtin [`.gitattributes` interface](https://git-scm.com/docs/gitattributes). So, if you were to invent a file format called a `Nicefile` at the root of your project that used ruby syntax, all you need is:
+
+``` conf
+/Nicefile gitlab-language=ruby
+```
+
+To disable highlighting entirely, use `gitlab-language=text`. Lots more fun shenanigans are available through CGI options, such as:
+
+``` conf
+# json with erb in it
+/my-cool-file gitlab-language=erb?parent=json
+
+# an entire file of highlighting errors!
+/other-file gitlab-language=text?token=Error
+```
+
+Please note that these configurations will only take effect when the `.gitattributes` file is in your default branch (usually `master`).
diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png
new file mode 100644
index 00000000000..e32a35f7cda
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_in_new_issue.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar.png b/doc/user/project/img/labels_assign_label_sidebar.png
new file mode 100644
index 00000000000..799443af889
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_sidebar.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar_saved.png b/doc/user/project/img/labels_assign_label_sidebar_saved.png
new file mode 100644
index 00000000000..e7d8d69e60e
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_sidebar_saved.png
Binary files differ
diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png
new file mode 100644
index 00000000000..ee0c9f889ad
--- /dev/null
+++ b/doc/user/project/img/labels_default.png
Binary files differ
diff --git a/doc/user/project/img/labels_description_tooltip.png b/doc/user/project/img/labels_description_tooltip.png
new file mode 100644
index 00000000000..0d1e3e091fb
--- /dev/null
+++ b/doc/user/project/img/labels_description_tooltip.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png
new file mode 100644
index 00000000000..ed622be2d93
--- /dev/null
+++ b/doc/user/project/img/labels_filter.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
new file mode 100644
index 00000000000..c5a9e20919b
--- /dev/null
+++ b/doc/user/project/img/labels_filter_by_priority.png
Binary files differ
diff --git a/doc/user/project/img/labels_generate.png b/doc/user/project/img/labels_generate.png
new file mode 100644
index 00000000000..9579be4e231
--- /dev/null
+++ b/doc/user/project/img/labels_generate.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png
new file mode 100644
index 00000000000..a916d3dceb5
--- /dev/null
+++ b/doc/user/project/img/labels_new_label.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly.png b/doc/user/project/img/labels_new_label_on_the_fly.png
new file mode 100644
index 00000000000..80cc434239e
--- /dev/null
+++ b/doc/user/project/img/labels_new_label_on_the_fly.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly_create.png b/doc/user/project/img/labels_new_label_on_the_fly_create.png
new file mode 100644
index 00000000000..c41090945eb
--- /dev/null
+++ b/doc/user/project/img/labels_new_label_on_the_fly_create.png
Binary files differ
diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png
new file mode 100644
index 00000000000..8dfe72cf826
--- /dev/null
+++ b/doc/user/project/img/labels_prioritize.png
Binary files differ
diff --git a/doc/user/project/img/labels_subscribe.png b/doc/user/project/img/labels_subscribe.png
new file mode 100644
index 00000000000..ea3db2bc0cf
--- /dev/null
+++ b/doc/user/project/img/labels_subscribe.png
Binary files differ
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
new file mode 100644
index 00000000000..4258185b7d0
--- /dev/null
+++ b/doc/user/project/labels.md
@@ -0,0 +1,147 @@
+# Labels
+
+Labels provide an easy way to categorize the issues or merge requests based on
+descriptive titles like `bug`, `documentation` or any other text you feel like
+it. They can have different colors, a description, and are visible throughout
+the issue tracker or inside each issue individually.
+
+With labels, you can navigate the issue tracker and filter any bloated
+information to visualize only the issues you are interested in. Let's see how
+that works.
+
+## Create new labels
+
+>**Note:**
+A permission level of `Developer` or higher is required in order to manage
+labels.
+
+Head over a single project and navigate to **Issues > Labels**.
+
+The first time you visit this page, you'll notice that there are no labels
+created yet.
+
+![Generate new labels](img/labels_generate.png)
+
+---
+
+You can skip that and create a new label or click that link and GitLab will
+generate a set of predefined labels for you. There 8 default generated labels
+in total and you can see them in the screenshot below.
+
+![Default generated labels](img/labels_default.png)
+
+---
+
+You can see that from the labels page you can have an overview of the number of
+issues and merge requests assigned to each label.
+
+Creating a new label from scratch is as easy as pressing the **New label**
+button. From there on you can choose the name, give it an optional description,
+a color and you are set.
+
+When you are ready press the **Create label** button to create the new label.
+
+![New label](img/labels_new_label.png)
+
+## Prioritize labels
+
+>**Notes:**
+ - This feature was introduced in GitLab 8.9.
+ - Priority sorting is based on the highest priority label only. This might
+ change in the future, follow the discussion in
+ https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
+
+Prioritized labels are like any other label, but sorted by priority. This allows
+you to sort issues and merge requests by priority.
+
+To prioritize labels, navigate to your project's **Issues > Labels** and click
+on the star icon next to them to put them in the priority list. Click on the
+star icon again to remove them from the list.
+
+From there, you can drag them around to set the desired priority. Priority is
+set from high to low with an ascending order. Labels with no priority, count as
+having their priority set to null.
+
+![Prioritize labels](img/labels_prioritize.png)
+
+Now that you have labels prioritized, you can use the 'Priority' filter in the
+issues or merge requests tracker. Those with the highest priority label, will
+appear on top.
+
+![Filter labels by priority](img/labels_filter_by_priority.png)
+
+## Subscribe to labels
+
+If you don’t want to miss issues or merge requests that are important to you,
+simply subscribe to a label. You’ll get notified whenever the label gets added
+to an issue or merge request, making sure you don’t miss a thing.
+
+Go to your project's **Issues > Labels** area, find the label(s) you want to
+subscribe to and click on the eye icon. Click again to unsubscribe.
+
+![Subscribe to labels](img/labels_subscribe.png)
+
+If you work on a large or popular project, try subscribing only to the labels
+that are relevant to you. You’ll notice it’ll be much easier to focus on what’s
+important.
+
+## Create a new label right from the issue tracker
+
+>**Note:**
+This feature was introduced in GitLab 8.6.
+
+There are times when you are already in the issue tracker searching for a
+label, only to realize it doesn't exist. Instead of going to the **Labels**
+page and being distracted from your original purpose, you can create new
+labels on the fly.
+
+Select **Create new** from the labels dropdown list, provide a name, pick a
+color and hit **Create**.
+
+![Create new label on the fly](img/labels_new_label_on_the_fly_create.png)
+![New label on the fly](img/labels_new_label_on_the_fly.png)
+
+## Assigning labels to issues and merge requests
+
+There are generally two ways to assign a label to an issue or merge request.
+
+You can assign a label when you first create or edit an issue or merge request.
+
+![Assign label in new issue](img/labels_assign_label_in_new_issue.png)
+
+---
+
+The second way is by using the right sidebar when inside an issue or merge
+request. Expand it and hit **Edit** in the labels area. Start typing the name
+of the label you are looking for to narrow down the list, and select it. You
+can add more than one labels at once. When done, click outside the sidebar area
+for the changes to take effect.
+
+![Assign label in sidebar](img/labels_assign_label_sidebar.png)
+![Save labels in sidebar](img/labels_assign_label_sidebar_saved.png)
+
+---
+
+To remove labels, expand the left sidebar and unmark them from the labels list.
+Simple as that.
+
+## Use labels to filter issues
+
+Once you start adding labels to your issues, you'll see the benefit of it.
+Labels can have several uses, one of them being the quick filtering of issues
+or merge requests.
+
+Pick an existing label from the dropdown _Label_ menu or click on an existing
+label from the issue tracker. In the latter case, you also get to see the
+label description like shown below.
+
+![Filter labels](img/labels_filter.png)
+
+---
+
+And if you added a description to your label, you can see it by hovering your
+mouse over the label in the issue tracker or wherever else the label is
+rendered.
+
+![Label tooltips](img/labels_description_tooltip.png)
+
diff --git a/doc/user/project/settings/img/import_export_download_export.png b/doc/user/project/settings/img/import_export_download_export.png
new file mode 100644
index 00000000000..a2f7f0085c1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_download_export.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_export_button.png b/doc/user/project/settings/img/import_export_export_button.png
new file mode 100644
index 00000000000..1f7bdd21b0d
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_export_button.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_mail_link.png b/doc/user/project/settings/img/import_export_mail_link.png
new file mode 100644
index 00000000000..c123f83eb8e
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_mail_link.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_new_project.png b/doc/user/project/settings/img/import_export_new_project.png
new file mode 100644
index 00000000000..b3a7f201018
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_new_project.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_select_file.png b/doc/user/project/settings/img/import_export_select_file.png
new file mode 100644
index 00000000000..f31832af3e1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_select_file.png
Binary files differ
diff --git a/doc/user/project/settings/img/settings_edit_button.png b/doc/user/project/settings/img/settings_edit_button.png
new file mode 100644
index 00000000000..3c0cee536de
--- /dev/null
+++ b/doc/user/project/settings/img/settings_edit_button.png
Binary files differ
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
new file mode 100644
index 00000000000..38e9786123d
--- /dev/null
+++ b/doc/user/project/settings/import_export.md
@@ -0,0 +1,73 @@
+# Project import/export
+
+>**Notes:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ Ask your administrator if you don't see the **GitLab export** button when
+ creating a new project.
+ - You can find some useful raketasks if you are an administrator in the
+ [import_export](../../../administration/raketasks/project_import_export.md)
+ raketask.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+Existing projects running on any GitLab instance or GitLab.com can be exported
+with all their related data and be moved into a new GitLab instance.
+
+## Exported contents
+
+The following items will be exported:
+
+- Project and wiki repositories
+- Project uploads
+- Project configuration including web hooks and services
+- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
+ and other project entities
+
+The following items will NOT be exported:
+
+- Build traces and artifacts
+- LFS objects
+
+## Exporting a project and its data
+
+1. Go to the project settings page by clicking on **Edit Project**:
+
+ ![Project settings button](img/settings_edit_button.png)
+
+1. Scroll down to find the **Export project** button:
+
+ ![Export button](img/import_export_export_button.png)
+
+1. Once the export is generated, you should receive an e-mail with a link to
+ download the file:
+
+ ![Email download link](img/import_export_mail_link.png)
+
+1. Alternatively, you can come back to the project settings and download the
+ file from there, or generate a new export. Once the file available, the page
+ should show the **Download export** button:
+
+ ![Download export](img/import_export_download_export.png)
+
+## Importing the project
+
+1. The new GitLab project import feature is at the far right of the import
+ options when creating a New Project. Make sure you are in the right namespace
+ and you have entered a project name. Click on **GitLab export**:
+
+ ![New project](img/import_export_new_project.png)
+
+1. You can see where the project will be imported to. You can now select file
+ exported previously:
+
+ ![Select file](img/import_export_select_file.png)
+
+1. Click on **Import project** to begin importing. Your newly imported project
+ page will appear soon.
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../../development/shared_files.md
diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png
index 698f1a0f64a..8c4f08d1825 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/web_hooks/ssl.png
Binary files differ
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index c1c51302e79..8559b67af04 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -13,6 +13,19 @@ You can configure webhooks to listen for specific events like pushes, issues or
Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
+## Webhook endpoint tips
+
+If you are writing your own endpoint (web server) that will receive
+GitLab webhooks keep in mind the following things:
+
+- Your endpoint should send its HTTP response as fast as possible. If
+ you wait too long, GitLab may decide the hook failed and retry it.
+- Your endpoint should ALWAYS return a valid HTTP response. If you do
+ not do this then GitLab will think the hook failed and retry it.
+ Most HTTP libraries take care of this for you automatically but if
+ you are writing a low-level hook this is important to remember.
+- GitLab ignores the HTTP status code returned by your endpoint.
+
## SSL Verification
By default, the SSL certificate of the webhook endpoint is verified based on
@@ -682,6 +695,61 @@ X-Gitlab-Event: Merge Request Hook
}
```
+## Wiki Page events
+
+Triggered when a wiki page is created or edited.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Wiki Page Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "wiki_page",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
+ },
+ "project": {
+ "name": "awesome-project",
+ "description": "This is awesome",
+ "web_url": "http://example.com/root/awesome-project",
+ "avatar_url": null,
+ "git_ssh_url": "git@example.com:root/awesome-project.git",
+ "git_http_url": "http://example.com/root/awesome-project.git",
+ "namespace": "root",
+ "visibility_level": 0,
+ "path_with_namespace": "root/awesome-project",
+ "default_branch": "master",
+ "homepage": "http://example.com/root/awesome-project",
+ "url": "git@example.com:root/awesome-project.git",
+ "ssh_url": "git@example.com:root/awesome-project.git",
+ "http_url": "http://example.com/root/awesome-project.git"
+ },
+ "wiki": {
+ "web_url": "http://example.com/root/awesome-project/wikis/home",
+ "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
+ "git_http_url": "http://example.com/root/awesome-project.wiki.git",
+ "path_with_namespace": "root/awesome-project.wiki",
+ "default_branch": "master"
+ },
+ "object_attributes": {
+ "title": "Awesome",
+ "content": "awesome content goes here",
+ "format": "markdown",
+ "message": "adding an awesome page to the wiki",
+ "slug": "awesome",
+ "url": "http://example.com/root/awesome-project/wikis/awesome",
+ "action": "create"
+ }
+}
+```
+
#### Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9efe41308dc..ddb2f7281b1 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -7,7 +7,7 @@
- [Groups](groups.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
-- [Labels](labels.md)
+- [Labels](../user/project/labels.md)
- [Notification emails](notifications.md)
- [Project Features](project_features.md)
- [Project forking workflow](forking_workflow.md)
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index fffa0aba57f..4b551130255 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -8,7 +8,7 @@ You should have `master` or `owner` permissions to add or import a new user
to your project.
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** in the drop-down menu on the right side of your screen.
![Members](img/add_user_members_menu.png)
@@ -87,3 +87,25 @@ invitation, change their access level or even delete them.
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.
+
+## Request access to a project
+
+As a user, you can request to be a member of a project. Go to the project you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](img/request_access_button.png)
+
+---
+
+Project owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](img/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](img/withdraw_access_request_button.png)
diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png
new file mode 100644
index 00000000000..5c9b510ba9d
--- /dev/null
+++ b/doc/workflow/add-user/img/access_requests_management.png
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
index 3da18cdae53..5da0552f9d6 100644
--- 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
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
index 910affc9659..a2954ad7c37 100644
--- a/doc/workflow/add-user/img/add_user_email_accept.png
+++ 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
index 5f02ce89b3e..19d91bc0999 100644
--- a/doc/workflow/add-user/img/add_user_email_ready.png
+++ 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
index 140979fbe13..cb31b77d941 100644
--- a/doc/workflow/add-user/img/add_user_email_search.png
+++ 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
index 8ef9156c8d5..e6b77022f06 100644
--- a/doc/workflow/add-user/img/add_user_give_permissions.png
+++ 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
index 5770d5cf0c4..1068589c5ff 100644
--- 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
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
index dea4b3f40ad..5cd120a4245 100644
--- a/doc/workflow/add-user/img/add_user_imported_members.png
+++ 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
index 7daa6ca7d9e..5fe3482192e 100644
--- a/doc/workflow/add-user/img/add_user_list_members.png
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png
index f1797b95f67..340d15c9830 100644
--- a/doc/workflow/add-user/img/add_user_members_menu.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
index 5ac10ce80d4..1c05d70ca31 100644
--- a/doc/workflow/add-user/img/add_user_search_people.png
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png
new file mode 100644
index 00000000000..984d640b0f0
--- /dev/null
+++ b/doc/workflow/add-user/img/request_access_button.png
Binary files differ
diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png
new file mode 100644
index 00000000000..ff54a0e4384
--- /dev/null
+++ b/doc/workflow/add-user/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 70b35c58be6..e6f8b792707 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,28 +1,26 @@
-# Award emojis
+# Award emoji
>**Note:**
This feature was [introduced][1825] in GitLab 8.2.
When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. In order to make virtual celebrations easier, you can now vote
-on issues and merge requests using emoji!
+and thumbs-ups. Emoji can be awarded to issues and merge requests, making
+virtual celebrations easier.
![Award emoji](img/award_emoji_select.png)
-This makes it much easier to give and receive feedback, without a long comment
-thread. Any comment that contains only the thumbs up or down emojis is
-converted to a vote and depicted in the emoji area.
-
-You can then use that functionality to sort issues and merge requests based on
-popularity.
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
## Sort issues and merge requests on vote count
>**Note:**
This feature was [introduced][2871] in GitLab 8.5.
-You can quickly sort the issues or merge requests by the number of votes they
-have received. The sort option can be found in the right dropdown menu.
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
![Votes sort options](img/award_emoji_votes_sort_options.png)
@@ -40,9 +38,28 @@ Sort by least popular issues/merge requests.
---
-The number of upvotes and downvotes is not summed up. That means that an issue
-with 18 upvotes and 5 downvotes is considered more popular than an issue with
-17 upvotes and no downvotes.
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+>**Note:**
+This feature was [introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+If you want to remove an award emoji, just click the emoji again and the vote
+will be removed.
[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
index fb26ee04393..481680af80c 100644
--- a/doc/workflow/award_emoji.png
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/ci_mr.png b/doc/workflow/ci_mr.png
index a577356f8e8..f8a7708643e 100644
--- a/doc/workflow/ci_mr.png
+++ b/doc/workflow/ci_mr.png
Binary files differ
diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/close_issue_mr.png
index a136d642e12..5e520240233 100644
--- a/doc/workflow/close_issue_mr.png
+++ b/doc/workflow/close_issue_mr.png
Binary files differ
diff --git a/doc/workflow/environment_branches.png b/doc/workflow/environment_branches.png
index ee893ced13b..13fb0478eaa 100644
--- a/doc/workflow/environment_branches.png
+++ b/doc/workflow/environment_branches.png
Binary files differ
diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png
index 275f64d113b..7f19414f3a9 100644
--- a/doc/workflow/forking/branch_select.png
+++ b/doc/workflow/forking/branch_select.png
Binary files differ
diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png
index 2dc00ed08a1..e2da42a2be7 100644
--- a/doc/workflow/forking/merge_request.png
+++ b/doc/workflow/forking/merge_request.png
Binary files differ
diff --git a/doc/workflow/four_stages.png b/doc/workflow/four_stages.png
index 2f444fc6f79..49413087dca 100644
--- a/doc/workflow/four_stages.png
+++ b/doc/workflow/four_stages.png
Binary files differ
diff --git a/doc/workflow/git_pull.png b/doc/workflow/git_pull.png
index 7d47064eb14..9a1fdf899bf 100644
--- a/doc/workflow/git_pull.png
+++ b/doc/workflow/git_pull.png
Binary files differ
diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/gitdashflow.png
index f2f091dd10b..e456cf9309d 100644
--- a/doc/workflow/gitdashflow.png
+++ b/doc/workflow/gitdashflow.png
Binary files differ
diff --git a/doc/workflow/github_flow.png b/doc/workflow/github_flow.png
index 88addb623ee..b3fca97cc2d 100644
--- a/doc/workflow/github_flow.png
+++ b/doc/workflow/github_flow.png
Binary files differ
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 1b354bcc0f1..2b2f140f8bf 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that
There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged.
If the assigned person does not feel comfortable they can close the merge request without merging.
-In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html).
+In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html).
So if you want to merge it into a protected branch you assign it to someone with master authorizations.
## Issues with GitLab flow
@@ -187,7 +187,7 @@ 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.
-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.
+In GitLab EE and .com you can also [rebase before merge](http://docs.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.
diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/gitlab_flow.png
index 1ea191a672b..d85d4ff374e 100644
--- a/doc/workflow/gitlab_flow.png
+++ b/doc/workflow/gitlab_flow.png
Binary files differ
diff --git a/doc/workflow/good_commit.png b/doc/workflow/good_commit.png
index 3737a026644..7958feea4d9 100644
--- a/doc/workflow/good_commit.png
+++ b/doc/workflow/good_commit.png
Binary files differ
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 52bf611dc5e..9b50286b179 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -51,10 +51,32 @@ If necessary, you can increase the access level of an individual user for a spec
![Barry effectively has 'Master' access to GitLab CI now](groups/override_access_level.png)
+## Requesting access to a group
+
+As a user, you can request to be a member of a group. Go to the group you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](groups/request_access_button.png)
+
+---
+
+Group owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](groups/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](groups/withdraw_access_request_button.png)
+
## Managing group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
-See [the GitLab Enterprise Edition documentation](http://doc.gitlab.com/ee/integration/ldap.html) for more information.
+See [the GitLab Enterprise Edition documentation](http://docs.gitlab.com/ee/integration/ldap.html) for more information.
## Allowing only admins to create groups
diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png
new file mode 100644
index 00000000000..5202434f00f
--- /dev/null
+++ b/doc/workflow/groups/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/groups/add_member_to_group.png b/doc/workflow/groups/add_member_to_group.png
index fa340ce572f..6e3f660d2e4 100644
--- a/doc/workflow/groups/add_member_to_group.png
+++ b/doc/workflow/groups/add_member_to_group.png
Binary files differ
diff --git a/doc/workflow/groups/group_dashboard.png b/doc/workflow/groups/group_dashboard.png
index 7fc9048d74d..662c932e536 100644
--- a/doc/workflow/groups/group_dashboard.png
+++ b/doc/workflow/groups/group_dashboard.png
Binary files differ
diff --git a/doc/workflow/groups/group_with_two_projects.png b/doc/workflow/groups/group_with_two_projects.png
index 87242781e4f..dc3475949f5 100644
--- a/doc/workflow/groups/group_with_two_projects.png
+++ b/doc/workflow/groups/group_with_two_projects.png
Binary files differ
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
index 71106a8a5a0..2855a514013 100644
--- a/doc/workflow/groups/max_access_level.png
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/new_group_button.png b/doc/workflow/groups/new_group_button.png
index 51e82798658..26136312c8f 100644
--- a/doc/workflow/groups/new_group_button.png
+++ b/doc/workflow/groups/new_group_button.png
Binary files differ
diff --git a/doc/workflow/groups/new_group_form.png b/doc/workflow/groups/new_group_form.png
index bf992c40bc2..dc50a069ef2 100644
--- a/doc/workflow/groups/new_group_form.png
+++ b/doc/workflow/groups/new_group_form.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
index cbf2c3c1fdc..2230720cecd 100644
--- a/doc/workflow/groups/other_group_sees_shared_project.png
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/override_access_level.png b/doc/workflow/groups/override_access_level.png
index f4225a63679..9d6aaf4c363 100644
--- a/doc/workflow/groups/override_access_level.png
+++ b/doc/workflow/groups/override_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/project_members_via_group.png b/doc/workflow/groups/project_members_via_group.png
index b13cb1cfd95..58270936a0b 100644
--- a/doc/workflow/groups/project_members_via_group.png
+++ b/doc/workflow/groups/project_members_via_group.png
Binary files differ
diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png
new file mode 100644
index 00000000000..0eec5cb937d
--- /dev/null
+++ b/doc/workflow/groups/request_access_button.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
index a5dbc89fe90..5772d4deced 100644
--- a/doc/workflow/groups/share_project_with_groups.png
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/groups/transfer_project.png b/doc/workflow/groups/transfer_project.png
index 044fe10d073..0aef3ab3f0f 100644
--- a/doc/workflow/groups/transfer_project.png
+++ b/doc/workflow/groups/transfer_project.png
Binary files differ
diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png
new file mode 100644
index 00000000000..b7de830a780
--- /dev/null
+++ b/doc/workflow/groups/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/workflow/img/award_emoji_comment_awarded.png
new file mode 100644
index 00000000000..67697831869
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/workflow/img/award_emoji_comment_picker.png
new file mode 100644
index 00000000000..d9c3faecdca
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
index fffdfedda5d..ad664c0aeff 100644
--- a/doc/workflow/img/award_emoji_select.png
+++ b/doc/workflow/img/award_emoji_select.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
index 2ef5be7154f..57d595d9602 100644
--- a/doc/workflow/img/award_emoji_votes_least_popular.png
+++ b/doc/workflow/img/award_emoji_votes_least_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
index 5b089730d93..432bd09b8a7 100644
--- a/doc/workflow/img/award_emoji_votes_most_popular.png
+++ b/doc/workflow/img/award_emoji_votes_most_popular.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
index 9bbf3f82a0b..ae6e224b317 100644
--- a/doc/workflow/img/award_emoji_votes_sort_options.png
+++ b/doc/workflow/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/workflow/img/cherry_pick_changes_commit.png
index ae91d2cae53..7fb68cc9e9b 100644
--- a/doc/workflow/img/cherry_pick_changes_commit.png
+++ b/doc/workflow/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/workflow/img/cherry_pick_changes_commit_modal.png
index f502f87677a..5267e04562f 100644
--- a/doc/workflow/img/cherry_pick_changes_commit_modal.png
+++ b/doc/workflow/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/workflow/img/cherry_pick_changes_mr.png
index 59c610e620b..975fb13e463 100644
--- a/doc/workflow/img/cherry_pick_changes_mr.png
+++ b/doc/workflow/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/workflow/img/cherry_pick_changes_mr_modal.png
index 96a80f4726d..6c003bacbe3 100644
--- a/doc/workflow/img/cherry_pick_changes_mr_modal.png
+++ b/doc/workflow/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/workflow/img/file_finder_find_button.png
index c5005d0d7ca..96e383f0213 100644
--- a/doc/workflow/img/file_finder_find_button.png
+++ 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
index 58500f4c163..c6508514c76 100644
--- a/doc/workflow/img/file_finder_find_file.png
+++ 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
index eefe5769554..1839d5e8be2 100644
--- a/doc/workflow/img/forking_workflow_choose_namespace.png
+++ 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
index 49e68d33e89..cc79d6fd40c 100644
--- a/doc/workflow/img/forking_workflow_fork_button.png
+++ 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
index 7a3139506fe..a859155aef0 100644
--- a/doc/workflow/img/forking_workflow_path_taken_error.png
+++ b/doc/workflow/img/forking_workflow_path_taken_error.png
Binary files differ
diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png
index 394c139e17e..61acdd30ae9 100644
--- a/doc/workflow/img/new_branch_from_issue.png
+++ b/doc/workflow/img/new_branch_from_issue.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/workflow/img/revert_changes_commit.png
index d84211e20db..e7194fc3504 100644
--- a/doc/workflow/img/revert_changes_commit.png
+++ 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
index e94d151a2af..c660ec7eaec 100644
--- a/doc/workflow/img/revert_changes_commit_modal.png
+++ 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
index 7adad88463b..3002f0ac1c5 100644
--- a/doc/workflow/img/revert_changes_mr.png
+++ 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
index 9da78f84828..c6aaeecc8a6 100644
--- a/doc/workflow/img/revert_changes_mr_modal.png
+++ b/doc/workflow/img/revert_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/img/todo_list_item.png b/doc/workflow/img/todo_list_item.png
new file mode 100644
index 00000000000..884ba1d22a3
--- /dev/null
+++ b/doc/workflow/img/todo_list_item.png
Binary files differ
diff --git a/doc/workflow/img/todos_add_todo_sidebar.png b/doc/workflow/img/todos_add_todo_sidebar.png
new file mode 100644
index 00000000000..126ecc2c82f
--- /dev/null
+++ b/doc/workflow/img/todos_add_todo_sidebar.png
Binary files differ
diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png
index 879b3b51c21..bba77f88913 100644
--- a/doc/workflow/img/todos_icon.png
+++ 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
index 4ee18dd1285..f1438ef7355 100644
--- a/doc/workflow/img/todos_index.png
+++ b/doc/workflow/img/todos_index.png
Binary files differ
diff --git a/doc/workflow/img/todos_mark_done_sidebar.png b/doc/workflow/img/todos_mark_done_sidebar.png
new file mode 100644
index 00000000000..f449f977dd6
--- /dev/null
+++ b/doc/workflow/img/todos_mark_done_sidebar.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
index 009e4b05adf..a8e635d2faf 100644
--- a/doc/workflow/img/web_editor_new_branch_dropdown.png
+++ 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
index dd6cfc6e7bb..7f36b7faf63 100644
--- a/doc/workflow/img/web_editor_new_branch_page.png
+++ 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
index 2c76f84f395..d16e3c67116 100644
--- a/doc/workflow/img/web_editor_new_directory_dialog.png
+++ 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
index cedf46aedfd..c8d77b16ee8 100644
--- a/doc/workflow/img/web_editor_new_directory_dropdown.png
+++ 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
index 6e884f6504d..3fcb91c9b93 100644
--- a/doc/workflow/img/web_editor_new_file_dropdown.png
+++ 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
index c76473bcfa7..21c340b9288 100644
--- a/doc/workflow/img/web_editor_new_file_editor.png
+++ 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
index a2108735741..c7738a4c930 100644
--- a/doc/workflow/img/web_editor_new_push_widget.png
+++ 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
index 263dd635b95..ac7415009b3 100644
--- a/doc/workflow/img/web_editor_new_tag_dropdown.png
+++ 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
index 64d7cd11ed1..231e1a13fc0 100644
--- a/doc/workflow/img/web_editor_new_tag_page.png
+++ 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
index be12a151cac..2755501dfd1 100644
--- a/doc/workflow/img/web_editor_start_new_merge_request.png
+++ 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
index 6dd2207bca0..9d6d8250bbe 100644
--- a/doc/workflow/img/web_editor_upload_file_dialog.png
+++ 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
index bf6528701b0..6b5205b05ec 100644
--- a/doc/workflow/img/web_editor_upload_file_dropdown.png
+++ b/doc/workflow/img/web_editor_upload_file_dropdown.png
Binary files differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
index 0e08703f421..1a5661de75d 100644
--- a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
+++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
index 205c515bd3f..fd7a4d3fabf 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
index a1e348d46ad..fd1ba6f5884 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
index ed362846909..186c1563951 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
index d2fbd0267bd..2f84d3232f2 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
index b1cc4b58525..652ca20b9ab 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png
index d2a286d8cac..35a7ddc8318 100644
--- a/doc/workflow/importing/gitlab_importer/importer.png
+++ b/doc/workflow/importing/gitlab_importer/importer.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png
index 5e239208e1e..81074d2d016 100644
--- a/doc/workflow/importing/gitlab_importer/new_project_page.png
+++ b/doc/workflow/importing/gitlab_importer/new_project_page.png
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
index f744dc06f81..b6ed8dd692a 100644
--- a/doc/workflow/importing/img/import_projects_from_github_importer.png
+++ 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
index 86be35acb37..c8f35a50f48 100644
--- 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
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index e670e415c71..a2b2a4b88f9 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,8 +1,10 @@
# Import your project from GitHub to GitLab
>**Note:**
-In order to enable the GitHub import setting, you should first
-enable the [GitHub integration][gh-import] in your GitLab instance.
+In order to enable the GitHub import setting, you may also want to
+enable the [GitHub integration][gh-import] in your GitLab instance. This
+configuration is optional, you will be able import your GitHub repositories
+with a Personal Access Token.
At its current state, GitHub importer can import:
@@ -20,9 +22,15 @@ It is not yet possible to import your cross-repository pull requests (those from
forks). We are working on improving this in the near future.
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.
+Click on the **GitHub** link and, if you are logged in via the GitHub
+integration, you will be redirected to GitHub for permission to access your
+projects. After accepting, you'll be automatically redirected to the importer.
+
+If you are not using the GitHub integration, you can still perform a one-off
+authorization with GitHub to access your projects.
+
+Alternatively, you can also enter a GitHub Personal Access Token. Once you enter
+your token, you'll be taken to the importer.
![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
@@ -44,5 +52,5 @@ 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"
+[ee-gh]: http://docs.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/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index 1117db98e7e..dcc00074b75 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -2,7 +2,7 @@
You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
GitLab support is enabled on your GitLab instance.
-You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html)
+You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
To get to the importer page you need to go to "New project" page.
![New project page](gitlab_importer/new_project_page.png)
diff --git a/doc/workflow/labels.md b/doc/workflow/labels.md
index 6e4840ca5ae..5c09891dfdd 100644
--- a/doc/workflow/labels.md
+++ b/doc/workflow/labels.md
@@ -1,18 +1,3 @@
# Labels
-In GitLab, you can easily tag issues and Merge Requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`.
-
-Here you can create a new label.
-
-![new label](labels/label1.png)
-
-You can choose to set a color.
-
-![label color](labels/label2.png)
-
-If you want to change an existing label, press edit next to the listed label.
-You will be presented with the same form as when creating a new label.
-
-![edit label](labels/label3.png)
-
-You can add labels to Merge Requests when you create or edit them.
+This document was moved to [user/project/labels.md](../user/project/labels.md).
diff --git a/doc/workflow/labels/label1.png b/doc/workflow/labels/label1.png
deleted file mode 100644
index cac661a34c8..00000000000
--- a/doc/workflow/labels/label1.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/labels/label2.png b/doc/workflow/labels/label2.png
deleted file mode 100644
index 44d9fef86d4..00000000000
--- a/doc/workflow/labels/label2.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/labels/label3.png b/doc/workflow/labels/label3.png
deleted file mode 100644
index e2fce11b7a4..00000000000
--- a/doc/workflow/labels/label3.png
+++ /dev/null
Binary files differ
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 83db44c10b1..9fe065fa680 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -4,7 +4,7 @@ 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)
+GitLab already supports [managing large files with git annex](http://docs.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.
@@ -127,7 +127,7 @@ To prevent this from happening, set the lfs url in project Git config:
```bash
-git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch"
+git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
```
### Credentials are always required when pushing an object
diff --git a/doc/workflow/merge_commits.png b/doc/workflow/merge_commits.png
index 757b589d0db..8aa1587cde6 100644
--- a/doc/workflow/merge_commits.png
+++ b/doc/workflow/merge_commits.png
Binary files differ
diff --git a/doc/workflow/merge_request.png b/doc/workflow/merge_request.png
index fde3ff5c854..6aad1d82f6e 100644
--- a/doc/workflow/merge_request.png
+++ b/doc/workflow/merge_request.png
Binary files differ
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 1b5718c91c1..d2ec56e6504 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -2,6 +2,17 @@
Merge requests allow you to exchange changes you made to source code
+## Only allow merge requests to be merged if the build succeeds
+
+You can prevent merge requests from being merged if their build did not succeed
+in the project settings page.
+
+![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png)
+
+Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box.
+
+Please note that you need to have builds configured to enable this feature.
+
## Checkout merge requests locally
Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/workflow/merge_requests/commit_compare.png
index dfd7ee220f0..0e4a2b23c04 100644
--- a/doc/workflow/merge_requests/commit_compare.png
+++ b/doc/workflow/merge_requests/commit_compare.png
Binary files differ
diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png
index f368423c746..3ebbfb75ea3 100644
--- a/doc/workflow/merge_requests/merge_request_diff.png
+++ b/doc/workflow/merge_requests/merge_request_diff.png
Binary files differ
diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
index b2d03bb66f9..a0db535019c 100644
--- a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
+++ b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
Binary files differ
diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
new file mode 100644
index 00000000000..18bebf5fe92
--- /dev/null
+++ b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/workflow/merge_when_build_succeeds/enable.png
index 633efa1246f..b86e6d7b3fd 100644
--- a/doc/workflow/merge_when_build_succeeds/enable.png
+++ b/doc/workflow/merge_when_build_succeeds/enable.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/workflow/merge_when_build_succeeds/status.png
index c856c7d14dc..f3ea61d8147 100644
--- a/doc/workflow/merge_when_build_succeeds/status.png
+++ b/doc/workflow/merge_when_build_succeeds/status.png
Binary files differ
diff --git a/doc/workflow/messy_flow.png b/doc/workflow/messy_flow.png
index 1addb95ca54..8d2c0dae8c2 100644
--- a/doc/workflow/messy_flow.png
+++ b/doc/workflow/messy_flow.png
Binary files differ
diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png
index de44c1ffc1a..3965ca4d083 100644
--- a/doc/workflow/milestones/form.png
+++ b/doc/workflow/milestones/form.png
Binary files differ
diff --git a/doc/workflow/milestones/group_form.png b/doc/workflow/milestones/group_form.png
index 38862dcca68..ff20df8081f 100644
--- a/doc/workflow/milestones/group_form.png
+++ b/doc/workflow/milestones/group_form.png
Binary files differ
diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/mr_inline_comments.png
index e851b95bcef..af7df3100d0 100644
--- a/doc/workflow/mr_inline_comments.png
+++ b/doc/workflow/mr_inline_comments.png
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 80817c98d22..b4a9c2f3d3e 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -4,7 +4,7 @@ GitLab has a notification system in place to notify a user of events that are im
## Notification settings
-Under user profile page you can find the notification settings.
+You can find notification settings under the user profile.
![notification settings](notifications/settings.png)
@@ -20,6 +20,7 @@ Each of these settings have levels of notification:
* Participating - receive notifications from related resources
* Watch - receive notifications from projects or groups user is a member of
* Global - notifications as set at the global settings
+* Custom - user will receive notifications when mentioned, is participant and custom selected events.
#### Global Settings
@@ -36,12 +37,14 @@ This means that you can set a different level of notifications per group while s
to have a finer level setting per project.
Organization like this is suitable for users that belong to different groups but don't have the
same need for being notified for every group they are member of.
+These settings can be configured on group page or user profile notifications dropdown.
#### Project Settings
Project Settings are at the top level and any setting placed at this level will take precedence of any
other setting.
This is suitable for users that have different needs for notifications per project basis.
+These settings can be configured on project page or user profile notifications dropdown.
## Notification events
@@ -55,7 +58,7 @@ Below is the table of events users can be notified of:
| User added to project | User | Sent when user is added to project |
| Project access level changed | User | Sent when user project access level is changed |
| User added to group | User | Sent when user is added to group |
-| Group access level changed | User | Sent when user group access level is changed |
+| Group access level changed | User | Sent when user group access level is changed |
| Project moved | Project members [1] | [1] not disabled |
### Issue / Merge Request events
@@ -69,8 +72,9 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher
-- Watchers: project members with notification level "Watch"
+- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
+- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
| Event | Sent to |
|------------------------|---------|
diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png
index e5b50ee2494..d50757beffc 100644
--- a/doc/workflow/notifications/settings.png
+++ b/doc/workflow/notifications/settings.png
Binary files differ
diff --git a/doc/workflow/production_branch.png b/doc/workflow/production_branch.png
index 33fb26dd621..d88a3687151 100644
--- a/doc/workflow/production_branch.png
+++ b/doc/workflow/production_branch.png
Binary files differ
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index d854ec1e025..67adfc2f43a 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -1,4 +1,4 @@
-# Protected branches
+# Protected Branches
Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches.
@@ -28,4 +28,28 @@ For those workflows, you can allow everyone with write access to push to a prote
On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box.
-![Developers can push](protected_branches/protected_branches2.png) \ No newline at end of file
+![Developers can push](protected_branches/protected_branches2.png)
+
+## Wildcard Protected Branches
+
+>**Note:**
+This feature was added in GitLab 8.10.
+
+1. You can specify a wildcard protected branch, which will protect all branches matching the wildcard. For example:
+
+ | Wildcard Protected Branch | Matching Branches |
+ |---------------------------+--------------------------------------------------------|
+ | `*-stable` | `production-stable`, `staging-stable` |
+ | `production/*` | `production/app-server`, `production/load-balancer` |
+ | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` |
+
+1. Protected branch settings (like "Developers Can Push") apply to all matching branches.
+
+1. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch.
+ >**Note:**
+ If _any_ of these protected branches have "Developers Can Push" set to true, then `production-stable` has it set to true.
+
+1. If you click on a protected branch's name, you will be presented with a list of all matching branches:
+
+ ![protected branch matches](protected_branches/protected_branches3.png)
+
diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png
index 5c2a3de5f70..c00443803de 100644
--- a/doc/workflow/protected_branches/protected_branches1.png
+++ b/doc/workflow/protected_branches/protected_branches1.png
Binary files differ
diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png
index 2dca3541365..a4f664d3b21 100644
--- a/doc/workflow/protected_branches/protected_branches2.png
+++ b/doc/workflow/protected_branches/protected_branches2.png
Binary files differ
diff --git a/doc/workflow/protected_branches/protected_branches3.png b/doc/workflow/protected_branches/protected_branches3.png
new file mode 100644
index 00000000000..2a50cb174bb
--- /dev/null
+++ b/doc/workflow/protected_branches/protected_branches3.png
Binary files differ
diff --git a/doc/workflow/rebase.png b/doc/workflow/rebase.png
index ef82c834755..df353311fa0 100644
--- a/doc/workflow/rebase.png
+++ b/doc/workflow/rebase.png
Binary files differ
diff --git a/doc/workflow/release_branches.png b/doc/workflow/release_branches.png
index da7ae53413a..c2162248d25 100644
--- a/doc/workflow/release_branches.png
+++ b/doc/workflow/release_branches.png
Binary files differ
diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png
index e2b64bfe17f..2456a8500f4 100644
--- a/doc/workflow/releases/new_tag.png
+++ b/doc/workflow/releases/new_tag.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png
index aca91906c68..eeda967afd6 100644
--- a/doc/workflow/releases/tags.png
+++ b/doc/workflow/releases/tags.png
Binary files differ
diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/remove_checkbox.png
index 3e247d38155..3b0393deb0f 100644
--- a/doc/workflow/remove_checkbox.png
+++ b/doc/workflow/remove_checkbox.png
Binary files differ
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
index a0ca6f14552..2c47625e29a 100644
--- a/doc/workflow/share_with_group.png
+++ b/doc/workflow/share_with_group.png
Binary files differ
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
index beb6c53ec77..a9b1c4b4dcc 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
index 5f440fdafdd..9524ffd5420 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -1,4 +1,4 @@
-# GitLab ToDos
+# GitLab Todos
>**Note:** This feature was [introduced][ce-2817] in GitLab 8.5.
@@ -14,8 +14,9 @@ in a simple dashboard.
---
-You can access quickly your Todos dashboard by clicking the round gray icon
-next to the search bar in the upper right corner.
+You can quickly access the Todos dashboard using the bell icon next to the
+search bar in the upper right corner. The number in blue is the number of Todos
+you still have open.
![Todos icon](img/todos_icon.png)
@@ -29,45 +30,61 @@ A Todo appears in your Todos dashboard when:
>**Note:** Commenting on a commit will _not_ trigger a Todo.
-## How a Todo is marked as Done
+### Manually creating a Todo
+
+You can also add an issue or merge request to your Todos dashboard by clicking
+the "Add Todo" button in the issue or merge request sidebar.
+
+![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png)
+
+## Marking a Todo as done
Any action to the corresponding issue or merge request will mark your Todo as
-**Done**. This action can include:
+**Done**. Actions that dismiss Todos 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.
+Todos are personal, and they're only marked as done if the action is coming from
+you. If you close the issue or merge request, your Todo will automatically
+be marked as done.
+
+If someone else closes, merges, or takes action on the issue or merge
+request, your Todo will remain pending. This prevents other users from closing issues without you being notified.
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.
+---
+
+If no action is needed, you can manually mark the Todo as done by clicking the
+corresponding **Done** button, and it will disappear from your Todo list.
+
+![A Todo in the Todos dashboard](img/todo_list_item.png)
+
+A Todo can also be marked as done from the issue or merge request sidebar using
+the "Mark Done" button.
+
+![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+
+You can mark all your Todos as done at once by clicking on the **Mark all as
+done** button.
+
## Filtering your Todos
-In general, there are four kinds of filters you can use on your Todos
-dashboard:
+There are four kinds of filters you can use on your Todos dashboard.
-| Filter | Description |
-| ------ | ----------- |
+| 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.
+You can also filter by more than one of these at the same time.
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/workflow/wip_merge_requests/blocked_accept_button.png
index 4791e5de972..89c458aa8d9 100644
--- a/doc/workflow/wip_merge_requests/blocked_accept_button.png
+++ b/doc/workflow/wip_merge_requests/blocked_accept_button.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/workflow/wip_merge_requests/mark_as_wip.png
index 8fa83a201ac..9c37354a653 100644
--- a/doc/workflow/wip_merge_requests/mark_as_wip.png
+++ b/doc/workflow/wip_merge_requests/mark_as_wip.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/workflow/wip_merge_requests/unmark_as_wip.png
index d45e68f31c5..31f7326beb0 100644
--- a/doc/workflow/wip_merge_requests/unmark_as_wip.png
+++ b/doc/workflow/wip_merge_requests/unmark_as_wip.png
Binary files differ
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
index 5de07e90e28..f5bb06dea7d 100644
--- a/features/admin/active_tab.feature
+++ b/features/admin/active_tab.feature
@@ -5,28 +5,36 @@ Feature: Admin Active Tab
Scenario: On Admin Home
Given I visit admin page
- Then the active main tab should be Home
+ Then the active main tab should be Overview
And no other main tabs should be active
Scenario: On Admin Projects
Given I visit admin projects page
- Then the active main tab should be Projects
+ Then the active main tab should be Overview
+ And the active sub tab should be Projects
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Groups
Given I visit admin groups page
- Then the active main tab should be Groups
+ Then the active main tab should be Overview
+ And the active sub tab should be Groups
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Users
Given I visit admin users page
- Then the active main tab should be Users
+ Then the active main tab should be Overview
+ And the active sub tab should be Users
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Logs
Given I visit admin logs page
- Then the active main tab should be Logs
+ Then the active main tab should be Monitoring
+ And the active sub tab should be Logs
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Messages
Given I visit admin messages page
@@ -40,5 +48,7 @@ Feature: Admin Active Tab
Scenario: On Admin Resque
Given I visit admin Resque page
- Then the active main tab should be Resque
+ Then the active main tab should be Monitoring
+ And the active sub tab should be Resque
And no other main tabs should be active
+ And no other sub tabs should be active
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index ab7de7ac315..657e847cf4a 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -27,13 +27,6 @@ Feature: Admin Groups
Then I should see project shared with group
@javascript
- Scenario: Remove user from group
- Given we have user "John Doe" in group
- When I visit admin group page
- And I remove user "John Doe" from group
- Then I should not see "John Doe" in team list
-
- @javascript
Scenario: Invite user to a group by e-mail
When I visit admin group page
When I select user "johndoe@gitlab.com" from user list as "Reporter"
diff --git a/features/admin/projects.feature b/features/admin/projects.feature
index c5ee80136c8..8929bcf8d80 100644
--- a/features/admin/projects.feature
+++ b/features/admin/projects.feature
@@ -10,10 +10,11 @@ Feature: Admin Projects
Then I should see all non-archived projects
And I should not see project "Archive"
+ @javascript
Scenario: I should see all projects in the list
Given archived project "Archive"
When I visit admin projects page
- And I check "Show archived projects"
+ And I select "Show archived projects"
Then I should see all projects
And I should see "archived" label
@@ -22,6 +23,7 @@ Feature: Admin Projects
And I click on first project
Then I should see project details
+ @javascript
Scenario: Transfer project
Given group 'Web'
And I visit admin project page
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index db73309804c..1f4c9020731 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -7,6 +7,7 @@ Feature: Dashboard
And project "Shop" has CI enabled
And project "Shop" has CI build
And project "Shop" has labels: "bug", "feature", "enhancement"
+ And project "Shop" has issue: "bug report"
And I visit dashboard page
Scenario: I should see projects list
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
index e3c01db2ebb..3ae2c679dc1 100644
--- a/features/dashboard/group.feature
+++ b/features/dashboard/group.feature
@@ -5,53 +5,9 @@ Feature: Dashboard Group
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
- # Leave groups
-
- @javascript
- Scenario: Owner should be able to leave from group if he is not the last owner
- Given "Mary Jane" is owner of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- And I visit dashboard groups page
- Then I should not see group "Owned" in group list
- Then I should see group "Guest" in group list
-
- @javascript
- Scenario: Owner should not be able to leave from group if he is the last owner
- Given "Mary Jane" is guest of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- Then I should see the "Can not leave message"
-
- @javascript
- Scenario: Guest should be able to leave from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
- @javascript
- Scenario: Guest should be able to leave from group even if he is the only user in the group
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
Scenario: Create a group from dasboard
And I visit dashboard groups page
And I click new group link
And submit form with new group "Samurai" info
Then I should be redirected to group "Samurai" page
And I should see newly created group "Samurai"
-
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index 76392068357..8ddafb6a7ac 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -14,14 +14,14 @@ Background:
@javascript
Scenario: I should see instructions on how to import from Git URL
Given I see "New Project" page
- When I click on "Any repo by URL"
+ When I click on "Repo by URL"
Then I see instructions on how to import from Git URL
@javascript
Scenario: I should see instructions on how to import from GitHub
Given I see "New Project" page
When I click on "Import project from GitHub"
- Then I see instructions on how to import from GitHub
+ Then I am redirected to the GitHub import page
@javascript
Scenario: I should see Google Code import page
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
index 8677b450813..42f5d6d2af7 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -14,7 +14,12 @@ Feature: Dashboard Todos
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 the todo marked as done
+
+ @javascript
+ Scenario: I mark all todos as done
+ Then I should see todos assigned to me
+ And I mark all todos as done
Then I should see all todos marked as done
@javascript
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 2fd097d100b..57dda9c2234 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -10,14 +10,9 @@ Feature: Project Active Tab
Then the active main tab should be Home
And no other main tabs should be active
- Scenario: On Project Files
+ Scenario: On Project Repository
Given I visit my project's files page
- Then the active main tab should be Files
- And no other main tabs should be active
-
- Scenario: On Project Commits
- Given I visit my project's commits page
- Then the active main tab should be Commits
+ Then the active main tab should be Repository
And no other main tabs should be active
Scenario: On Project Issues
@@ -30,11 +25,6 @@ Feature: Project Active Tab
Then the active main tab should be Merge Requests
And no other main tabs should be active
- Scenario: On Project Members
- Given I visit my project's members page
- Then the active main tab should be Members
- And no other main tabs should be active
-
Scenario: On Project Wiki
Given I visit my project's wiki page
Then the active main tab should be Wiki
@@ -49,13 +39,6 @@ Feature: Project Active Tab
# Sub Tabs: Settings
- Scenario: On Project Settings/Edit
- Given I visit my project's settings page
- And I click the "Edit" tab
- Then the active sub nav should be Edit
- And no other sub navs should be active
- And the active main tab should be Settings
-
Scenario: On Project Settings/Hooks
Given I visit my project's settings page
And I click the "Hooks" tab
@@ -70,40 +53,52 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
- # Sub Tabs: Commits
+ Scenario: On Project Members
+ Given I visit my project's members page
+ Then the active sub nav should be Members
+ And no other sub navs should be active
+ And the active main tab should be Settings
- Scenario: On Project Commits/Commits
+ # Sub Tabs: Repository
+
+ Scenario: On Project Repository/Files
+ Given I visit my project's files page
+ Then the active sub tab should be Files
+ And no other sub tabs should be active
+ And the active main tab should be Repository
+
+ Scenario: On Project Repository/Commits
Given I visit my project's commits page
Then the active sub tab should be Commits
And no other sub tabs should be active
- And the active main tab should be Commits
+ And the active main tab should be Repository
- Scenario: On Project Commits/Network
+ Scenario: On Project Repository/Network
Given I visit my project's network page
Then the active sub tab should be Network
And no other sub tabs should be active
- And the active main tab should be Commits
+ And the active main tab should be Repository
- Scenario: On Project Commits/Compare
+ Scenario: On Project Repository/Compare
Given I visit my project's commits page
And I click the "Compare" tab
Then the active sub tab should be Compare
And no other sub tabs should be active
- And the active main tab should be Commits
+ And the active main tab should be Repository
- Scenario: On Project Commits/Branches
+ Scenario: On Project Repository/Branches
Given I visit my project's commits page
And I click the "Branches" tab
Then the active sub tab should be Branches
And no other sub tabs should be active
- And the active main tab should be Commits
+ And the active main tab should be Repository
- Scenario: On Project Commits/Tags
+ Scenario: On Project Repository/Tags
Given I visit my project's commits page
And I click the "Tags" tab
Then the active sub tab should be Tags
And no other sub tabs should be active
- And the active main tab should be Commits
+ And the active main tab should be Repository
Scenario: On Project Issues/Browse
Given I visit my project's issues page
@@ -112,12 +107,16 @@ Feature: Project Active Tab
Scenario: On Project Issues/Milestones
Given I visit my project's issues page
- And I click the "Milestones" tab
- Then the active main tab should be Milestones
+ And I click the "Milestones" sub tab
+ Then the active main tab should be Issues
+ Then the active sub tab should be Milestones
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Project Issues/Labels
Given I visit my project's issues page
- And I click the "Labels" tab
- Then the active main tab should be Labels
+ And I click the "Labels" sub tab
+ Then the active main tab should be Issues
+ Then the active sub tab should be Labels
And no other main tabs should be active
+ And no other sub tabs should be active
diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature
index 3c029a973df..550ebccf0d7 100644
--- a/features/project/builds/summary.feature
+++ b/features/project/builds/summary.feature
@@ -24,3 +24,4 @@ Feature: Project Builds Summary
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
+ And the build count cache is updated
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index de7e2b37725..358e622b736 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -25,13 +25,6 @@ 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 filter by author
@@ -226,8 +219,8 @@ Feature: Project Issues
When I click button "Unsubscribe"
Then I should see that I am unsubscribed
+ @javascript
Scenario: I submit new unassigned issue as guest
- Given I logout
Given public project "Community"
When I visit project "Community" page
And I visit project "Community" issues page
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index ecda4ea8240..21768c15c17 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -49,14 +49,12 @@ Feature: Project Merge Requests
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,18 +74,6 @@ Feature: Project Merge Requests
And I submit new merge request "Wiki Feature"
Then I should see merge request "Wiki Feature"
- Scenario: I download a diff on a public merge request
- Given public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Bug CO-01" open merge request with diffs inside
- Given I logout directly
- And I visit merge request page "Bug CO-01"
- And I click on "Email Patches"
- Then I should see a patch diff
- And I visit merge request page "Bug CO-01"
- And I click on "Plain Diff"
- Then I should see a patch diff
-
@javascript
Scenario: I comment on a merge request
Given I visit merge request page "Bug NS-04"
@@ -103,13 +89,6 @@ Feature: Project Merge Requests
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"
diff --git a/features/project/project.feature b/features/project/project.feature
index f1f3ed26065..aa22401c88e 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -18,15 +18,6 @@ Feature: Project
Then I should see the default project avatar
And I should not see the "Remove avatar" button
- Scenario: I should have back to group button
- And project "Shop" belongs to group
- And I visit project "Shop" page
- Then I should see back to group button
-
- Scenario: I should have back to group button
- And I visit project "Shop" page
- Then I should see back to dashboard button
-
Scenario: I should have readme on page
And I visit project "Shop" page
Then I should see project "Shop" README
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index 10e7c234610..f71f69ef060 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -8,19 +8,21 @@ Feature: Project Shortcuts
@javascript
Scenario: Navigate to files tab
Given I press "g" and "f"
- Then the active main tab should be Files
+ Then the active main tab should be Repository
+ Then the active sub tab should be Files
@javascript
Scenario: Navigate to commits tab
Given I visit my project's files page
Given I press "g" and "c"
- Then the active main tab should be Commits
+ Then the active main tab should be Repository
+ Then the active sub tab should be Commits
@javascript
Scenario: Navigate to network tab
Given I press "g" and "n"
Then the active sub tab should be Network
- And the active main tab should be Commits
+ And the active main tab should be Repository
@javascript
Scenario: Navigate to graphs tab
diff --git a/features/search.feature b/features/search.feature
index a946a836525..818ef436db6 100644
--- a/features/search.feature
+++ b/features/search.feature
@@ -73,13 +73,15 @@ Feature: Search
Scenario: I logout and should see project I am looking for
Given project "Shop" is public
- And I logout
+ And I logout directly
+ And I visit dashboard search page
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 I logout directly
+ And I visit dashboard search page
And project has issues
When I search for "Foo"
And I click "Issues" link
@@ -88,7 +90,7 @@ Feature: Search
Scenario: I logout and should see project code I am looking for
Given project "Shop" is public
- And I logout
+ And I logout directly
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/active_tab.rb b/features/steps/admin/active_tab.rb
index 90d13abdb13..9b1689a8198 100644
--- a/features/steps/admin/active_tab.rb
+++ b/features/steps/admin/active_tab.rb
@@ -3,32 +3,36 @@ class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
include SharedPaths
include SharedActiveTab
- step 'the active main tab should be Home' do
+ step 'the active main tab should be Overview' do
ensure_active_main_tab('Overview')
end
- step 'the active main tab should be Projects' do
- ensure_active_main_tab('Projects')
+ step 'the active sub tab should be Projects' do
+ ensure_active_sub_tab('Projects')
end
- step 'the active main tab should be Groups' do
- ensure_active_main_tab('Groups')
+ step 'the active sub tab should be Groups' do
+ ensure_active_sub_tab('Groups')
end
- step 'the active main tab should be Users' do
- ensure_active_main_tab('Users')
- end
-
- step 'the active main tab should be Logs' do
- ensure_active_main_tab('Logs')
+ step 'the active sub tab should be Users' do
+ ensure_active_sub_tab('Users')
end
step 'the active main tab should be Hooks' do
ensure_active_main_tab('Hooks')
end
- step 'the active main tab should be Resque' do
- ensure_active_main_tab('Background Jobs')
+ step 'the active main tab should be Monitoring' do
+ ensure_active_main_tab('Monitoring')
+ end
+
+ step 'the active sub tab should be Resque' do
+ ensure_active_sub_tab('Background Jobs')
+ end
+
+ step 'the active sub tab should be Logs' do
+ ensure_active_sub_tab('Logs')
end
step 'the active main tab should be Messages' do
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index e1f1db2872f..0c89a3db9ad 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -62,7 +62,8 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
page.within ".group-users-list" do
- expect(page).to have_content "johndoe@gitlab.com (invited)"
+ expect(page).to have_content "johndoe@gitlab.com"
+ expect(page).to have_content "Invited by"
expect(page).to have_content "Reporter"
end
end
@@ -92,12 +93,6 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
current_group.add_reporter(user_john)
end
- step 'I remove user "John Doe" from group' do
- page.within "#user_#{user_john.id}" do
- click_link 'Remove user from group'
- end
- end
-
step 'I should not see "John Doe" in team list' do
page.within ".group-users-list" do
expect(page).not_to have_content "John Doe"
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
index a7a28755a6c..d77945a6b9c 100644
--- a/features/steps/admin/projects.rb
+++ b/features/steps/admin/projects.rb
@@ -18,9 +18,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
end
end
- step 'I check "Show archived projects"' do
- page.check 'Show archived projects'
- click_button "Search"
+ step 'I select "Show archived projects"' do
+ find(:css, '#sort-projects-dropdown').click
+ click_link 'Show archived projects'
end
step 'I should see "archived" label' do
@@ -45,7 +45,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
step 'I transfer project to group \'Web\'' do
allow_any_instance_of(Projects::TransferService).
to receive(:move_uploads_to_new_namespace).and_return(true)
- find(:xpath, "//input[@id='new_namespace_id']").set group.id
+ click_button 'Search for Namespace'
+ click_link 'group: web'
click_button 'Transfer'
end
diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb
index 4bc290b6bdf..8fb8a86d58b 100644
--- a/features/steps/admin/users.rb
+++ b/features/steps/admin/users.rb
@@ -158,7 +158,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps
step 'I should not see twitter details' do
expect(page).to have_content 'Pete'
- expect(page).to_not have_content 'twitter'
+ expect(page).not_to have_content 'twitter'
end
step 'click on ssh keys tab' do
diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb
index 0e2c04fb299..04fe96cef22 100644
--- a/features/steps/dashboard/active_tab.rb
+++ b/features/steps/dashboard/active_tab.rb
@@ -1,9 +1,5 @@
class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
- end
+ include SharedSidebarActiveTab
end
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index b5980b35102..80ed4c6d64c 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I should see "Shop" project CI status' do
- expect(page).to have_link "Build skipped"
+ expect(page).to have_link "Commit: skipped"
end
step 'I should see last push widget' do
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
index 0c6a0ae3725..cf679fea530 100644
--- a/features/steps/dashboard/group.rb
+++ b/features/steps/dashboard/group.rb
@@ -4,44 +4,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
include SharedPaths
include SharedUser
- # Leave
-
- step 'I click on the "Leave" button for group "Owned"' do
- find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Leave" button for group "Guest"' do
- find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for group "Owned"' do
- expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for groupr "Guest"' do
- expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should see group "Owned" in group list' do
- expect(page).to have_content("Owned")
- end
-
- step 'I should not see group "Owned" in group list' do
- expect(page).not_to have_content("Owned")
- end
-
- step 'I should see group "Guest" in group list' do
- expect(page).to have_content("Guest")
- end
-
- step 'I should not see group "Guest" in group list' do
- expect(page).not_to have_content("Guest")
- end
-
step 'I click new group link' do
click_link "New Group"
end
@@ -60,8 +22,4 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
expect(page).to have_content "Samurai"
expect(page).to have_content "Tokugawa Shogunate"
end
-
- step 'I should see the "Can not leave message"' do
- expect(page).to have_content "You can not leave Owned group because you're the last owner"
- end
end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index e21af72a777..8706f0e8e78 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -74,7 +74,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
def project
@project ||= begin
- project =create :project
+ project = create :project
project.team << [current_user, :master]
project
end
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index a2adc87f8ef..06db36c7014 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -100,7 +100,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
def project
@project ||= begin
- project =create :project
+ project = create :project
project.team << [current_user, :master]
project
end
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index a0aad66184d..727a6a71373 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -11,6 +11,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
step 'I see "New Project" page' do
expect(page).to have_content('Project path')
+ expect(page).to have_content('Project name')
end
step 'I see all possible import optios' do
@@ -19,24 +20,19 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code')
- expect(page).to have_link('Any repo by URL')
+ expect(page).to have_link('Repo by URL')
+ expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
first('.import_github').click
end
- step 'I see instructions on how to import from GitHub' do
- github_modal = first('.modal-body')
- expect(github_modal).to be_visible
- expect(github_modal).to have_content "To enable importing projects from GitHub"
-
- page.all('.modal-body').each do |element|
- expect(element).not_to be_visible unless element == github_modal
- end
+ step 'I am redirected to the GitHub import page' do
+ expect(current_path).to eq new_import_github_path
end
- step 'I click on "Any repo by URL"' do
+ step 'I click on "Repo by URL"' do
first('.import_git').click
end
@@ -53,5 +49,4 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
step 'I redirected to Google Code import page' do
expect(current_path).to eq new_import_google_code_path
end
-
end
diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb
index a9083850b52..118d27888df 100644
--- a/features/steps/dashboard/shortcuts.rb
+++ b/features/steps/dashboard/shortcuts.rb
@@ -2,5 +2,6 @@ class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
- include SharedActiveTab
+ include SharedSidebarActiveTab
+ include SharedShortcuts
end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 2b23df6764b..60152d3da55 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -20,21 +20,21 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
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?")
+ note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?", project: project)
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
- page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' }
+ page.within('.todos-pending-count') { expect(page).to have_content '4' }
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.to_reference}", 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)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?")
+ should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title)
end
step 'I mark the todo as done' do
@@ -42,19 +42,40 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
- page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' }
+ page.within('.todos-pending-count') { expect(page).to have_content '3' }
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.to_reference}"
end
- step 'I click on the "Done" tab' do
+ step 'I mark all todos as done' do
+ click_link 'Mark all as done'
+
+ page.within('.todos-pending-count') { expect(page).to have_content '0' }
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 4'
+ expect(page).not_to have_link project.name_with_namespace
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ end
+
+ step 'I should see the todo marked as done' do
click_link 'Done 1'
+
+ expect(page).to have_link project.name_with_namespace
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
end
step 'I should see all todos marked as done' do
+ click_link 'Done 4'
+
expect(page).to have_link project.name_with_namespace
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false)
+ should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false)
end
step 'I filter by "Enterprise"' do
@@ -78,7 +99,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
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}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
end
step 'I should not see todos related to "Merge Requests" in the list' do
@@ -87,7 +108,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
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.to_reference}"
- should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
end
step 'I click on the todo' do
@@ -106,7 +127,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
if pending
expect(page).to have_link 'Done'
else
- expect(page).to_not have_link 'Done'
+ expect(page).not_to have_link 'Done'
end
end
end
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index cb6fa8a47da..2b4a5ab0864 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -69,7 +69,6 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
visit namespace_project_issues_path(public_project.namespace, public_project)
end
-
step 'I should see list of issues for "Community" project' do
expect(page).to have_content "Bug"
expect(page).to have_content public_project.name
@@ -88,7 +87,6 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
visit namespace_project_issues_path(internal_project.namespace, internal_project)
end
-
step 'I should see list of issues for "Internal" project' do
expect(page).to have_content "Internal Bug"
expect(page).to have_content internal_project.name
@@ -137,7 +135,6 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
@public_project ||= Project.find_by!(name: 'Community')
end
-
def internal_merge_request
@internal_merge_request ||= MergeRequest.find_by!(title: 'Feature implemented')
end
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index 0706df3aec5..dfa2fa75def 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
+ expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
@@ -116,11 +116,9 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- find(".js-toggle-button").click
- page.within "#edit_group_member_#{member.id}" do
- select 'Developer', from: 'group_member_access_level'
- click_on 'Save'
- end
+ click_button "Edit access level"
+ select 'Developer', from: 'group_member_access_level'
+ click_on 'Save'
end
end
@@ -128,9 +126,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- page.within '.member-access-level' do
- expect(page).to have_content "Developer"
- end
+ expect(page).to have_content "Developer"
end
end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 483370f41c6..4fa7d7c6567 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -93,7 +93,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I should see new group "Owned" avatar' do
expect(owned_group.avatar).to be_instance_of AvatarUploader
- expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name:"Owned").id}/banana_sample.gif"
+ expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb
index 3b59089a093..4724a326277 100644
--- a/features/steps/profile/active_tab.rb
+++ b/features/steps/profile/active_tab.rb
@@ -22,8 +22,4 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
step 'the active main tab should be Audit Log' do
ensure_active_main_tab('Audit Log')
end
-
- def ensure_active_main_tab(content)
- expect(find('.layout-nav li.active')).to have_content(content)
- end
end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index a96f35ada51..7e339443b75 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,12 +11,10 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- select 'mention', from: 'notification_setting_level'
+ first(:link, "On mention").trigger('click')
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
- end
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 909de31a479..4ee6784a086 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -155,7 +155,11 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I click on my profile picture' do
- find(:css, '.sidebar-user').click
+ find(:css, '.header-user-dropdown-toggle').click
+
+ page.within ".header-user" do
+ click_link "Profile"
+ end
end
step 'I should see my user page' do
@@ -166,7 +170,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I have group with projects' do
- @group = create(:group)
+ @group = create(:group)
@group.add_owner(current_user)
@project = create(:project, namespace: @group)
@event = create(:closed_issue_event, project: @project)
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 19d81453d8c..80043463188 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -16,12 +16,14 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Snippets" tab' do
- click_link('Snippets')
+ page.within('.layout-nav') do
+ click_link('Snippets')
+ end
end
- step 'I click the "Edit" tab' do
- page.within '.sidebar-subnav' do
- click_link('Project Settings')
+ step 'I click the "Edit Project"' do
+ page.within '.layout-nav .controls' do
+ click_link('Edit Project')
end
end
@@ -33,14 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
click_link('Deploy Keys')
end
- step 'the active sub nav should be Team' do
+ step 'the active sub nav should be Members' do
ensure_active_sub_nav('Members')
end
- step 'the active sub nav should be Edit' do
- ensure_active_sub_nav('Project')
- end
-
step 'the active sub nav should be Hooks' do
ensure_active_sub_nav('Webhooks')
end
@@ -56,17 +54,15 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Branches" tab' do
- click_link('Branches')
+ page.within '.content' do
+ click_link('Branches')
+ end
end
step 'I click the "Tags" tab' do
click_link('Tags')
end
- step 'the active sub tab should be Commits' do
- ensure_active_sub_tab('Commits')
- end
-
step 'the active sub tab should be Compare' do
ensure_active_sub_tab('Compare')
end
@@ -81,23 +77,27 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues
- step 'I click the "Milestones" tab' do
- click_link('Milestones')
+ step 'I click the "Milestones" sub tab' do
+ page.within('.sub-nav') do
+ click_link('Milestones')
+ end
end
- step 'I click the "Labels" tab' do
- click_link('Labels')
+ step 'I click the "Labels" sub tab' do
+ page.within('.sub-nav') do
+ click_link('Labels')
+ end
end
step 'the active sub tab should be Issues' do
ensure_active_sub_tab('Issues')
end
- step 'the active main tab should be Milestones' do
- ensure_active_main_tab('Milestones')
+ step 'the active sub tab should be Milestones' do
+ ensure_active_sub_tab('Milestones')
end
- step 'the active main tab should be Labels' do
- ensure_active_main_tab('Labels')
+ step 'the active sub tab should be Labels' do
+ ensure_active_sub_tab('Labels')
end
end
diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb
index db1387763d5..b6f1d417e21 100644
--- a/features/steps/project/archived.rb
+++ b/features/steps/project/archived.rb
@@ -33,5 +33,4 @@ class Spinach::Features::ProjectArchived < Spinach::FeatureSteps
When 'I set project unarchived' do
click_link "Unarchive"
end
-
end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 1bdb57af9d1..2876e8812e9 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -5,11 +5,11 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
include RepoHelpers
step 'I click artifacts download button' do
- page.within('.artifacts') { click_link 'Download' }
+ click_link 'Download'
end
step 'I click artifacts browse button' do
- page.within('.artifacts') { click_link 'Browse' }
+ click_link 'Browse'
end
step 'I should see content of artifacts archive' do
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
index e9e2359146e..374eb0b0e07 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
expect(page).to have_content 'Build has been erased'
end
end
+
+ step 'the build count cache is updated' do
+ expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all)
+ end
end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 93c37bf507f..239036e431d 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -105,7 +105,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should not see button to create a new merge request' do
- expect(page).to_not have_link 'Create Merge Request'
+ expect(page).not_to have_link 'Create Merge Request'
end
step 'I should see button to the merge request' do
@@ -164,16 +164,16 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'commit has ci status' do
@project.enable_ci
- ci_commit = create :ci_commit, project: @project, sha: sample_commit.id
- create :ci_build, commit: ci_commit
+ pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id
+ create :ci_build, pipeline: pipeline
end
step 'repository contains ".gitlab-ci.yml" file' do
- allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file).and_return(String.new)
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(String.new)
end
step 'I see commit ci info' do
- expect(page).to have_content "build: pending"
+ expect(page).to have_content "Builds for 1 pipeline pending"
end
step 'I click status link' do
@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see builds list' do
- expect(page).to have_content "build: pending"
+ expect(page).to have_content "Builds for 1 pipeline pending"
expect(page).to have_content "1 build"
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 527f7853da9..8abeb5ee242 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I goto the Merge Requests page' do
- page.within '.page-sidebar-expanded' do
+ page.within '.layout-nav' do
click_link "Merge Requests"
end
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 0ead83d6937..6b56a77b832 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -153,6 +153,6 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
# Verify a link is generated against the correct project
def verify_commit_link(container_div, container_project)
# This should force a wait for the javascript to execute
- expect(find(:div,container_div).find(".commit_short_id")['href']).to have_content "#{container_project.path_with_namespace}/commit"
+ expect(find(:div, container_div).find(".commit_short_id")['href']).to have_content "#{container_project.path_with_namespace}/commit"
end
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index b1ffe7f7b4c..13c0713669a 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -59,7 +59,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'hook should be triggered' do
expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
expect(page).to have_selector '.flash-notice',
- text: 'Hook successfully executed.'
+ text: 'Hook executed successfully: HTTP 200'
end
step 'I should see hook error message' do
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index c5d45709b44..1498f899cf5 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -39,8 +39,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I can see the activity and food categories' do
page.within '.emoji-menu' do
- expect(page).to_not have_selector 'Activity'
- expect(page).to_not have_selector 'Food'
+ expect(page).not_to have_selector 'Activity'
+ expect(page).not_to have_selector 'Food'
end
end
@@ -81,9 +81,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I search "hand"' do
- page.within('.emoji-menu-content') do
- fill_in 'emoji_search', with: 'hand'
- end
+ fill_in 'emoji_search', with: 'hand'
end
step 'I see search result for "hand"' do
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index d82c6856918..d34fa694789 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
- page.find('.js-label-select').click
+ page.find('.js-label-select', visible: true).click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index fc12843ea5c..35f166c7c08 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -124,14 +124,12 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'project "Shop" has milestone "v2.2"' do
-
milestone = create(:milestone, title: "v2.2", project: project)
3.times { create(:issue, project: project, milestone: milestone) }
end
step 'project "Shop" has milestone "v3.0"' do
-
milestone = create(:milestone, title: "v3.0", project: project)
3.times { create(:issue, project: project, milestone: milestone) }
@@ -147,7 +145,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
When 'I select first assignee from "Shop" project' do
-
first_assignee = project.users.first
select first_assignee.name, from: "assignee_id"
end
@@ -160,7 +157,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'project "Shop" have "Release 0.4" open issue' do
-
create(:issue,
title: "Release 0.4",
project: project,
@@ -191,15 +187,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
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)
+ awardable = Issue.find_by(title: 'Release 0.4')
+ create_list(:award_emoji, 2, awardable: awardable)
+ create(:award_emoji, :downvote, awardable: awardable)
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)
+ awardable = Issue.find_by(title: 'Tweet control')
+ create(:award_emoji, :upvote, awardable: awardable)
+ create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
end
step 'The list should be sorted by "Least popular"' do
@@ -216,7 +212,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
page.within 'li.issue:nth-child(3)' do
expect(page).to have_content 'Bugfix'
- expect(page).to_not have_content '0 0'
+ expect(page).not_to have_content '0 0'
end
end
end
@@ -235,7 +231,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
page.within 'li.issue:nth-child(3)' do
expect(page).to have_content 'Bugfix'
- expect(page).to_not have_content '0 0'
+ expect(page).not_to have_content '0 0'
end
end
end
@@ -348,7 +344,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do
issue = Issue.find_by!(title: 'Release 0.4')
- create(:note_on_issue, noteable: issue, note: 'Yay!')
+ create(:note_on_issue, noteable: issue, project: project, note: 'Yay!')
end
step 'I should see a new comment with text "Yay!"' do
@@ -360,5 +356,4 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
def filter_issue(text)
fill_in 'issue_search', with: text
end
-
end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index 0ca2d6257c3..2937d5d7ca8 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do
- click_link 'Delete'
+ first(:link, 'Delete').click
end
end
@@ -24,8 +24,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I should see labels help message' do
page.within '.labels' do
- expect(page).to have_content 'Create first label or generate default set of '\
- 'labels'
+ expect(page).to have_content 'Create a label or generate a default set '\
+ 'of labels'
end
end
@@ -60,25 +60,25 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end
step 'I should see label \'feature\'' do
- page.within '.manage-labels-list' do
+ page.within '.other-labels .manage-labels-list' do
expect(page).to have_content 'feature'
end
end
step 'I should see label \'bug\'' do
- page.within '.manage-labels-list' do
+ page.within '.other-labels .manage-labels-list' do
expect(page).to have_content 'bug'
end
end
step 'I should not see label \'bug\'' do
- page.within '.manage-labels-list' do
+ page.within '.other-labels .manage-labels-list' do
expect(page).not_to have_content 'bug'
end
end
step 'I should see label \'support\'' do
- page.within '.manage-labels-list' do
+ page.within '.other-labels .manage-labels-list' do
expect(page).to have_content 'support'
end
end
@@ -90,7 +90,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end
step 'I should see label \'fix\'' do
- page.within '.manage-labels-list' do
+ page.within '.other-labels .manage-labels-list' do
expect(page).to have_content 'fix'
end
end
diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb
index 5bb02189021..118ffef4774 100644
--- a/features/steps/project/labels.rb
+++ b/features/steps/project/labels.rb
@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
private
def subscribe_button
- first('.label-subscribe-button span')
+ first('.js-subscribe-button', visible: true)
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 3b1a00f628a..da848afd48e 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
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)
+ create_list(:award_emoji, 2, awardable: merge_request)
+ create(:award_emoji, :downvote, awardable: 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)
+ awardable = MergeRequest.find_by(title: 'Bug NS-06')
+ create(:award_emoji, awardable: awardable)
+ create_list(:award_emoji, 2, :downvote, awardable: awardable)
end
step 'The list should be sorted by "Least popular"' do
@@ -203,7 +203,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
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'
+ expect(page).not_to have_content '0 0'
end
end
end
@@ -222,7 +222,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
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'
+ expect(page).not_to have_content '0 0'
end
end
end
@@ -272,10 +272,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
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,
+ create(:diff_note_on_merge_request, project: project,
+ noteable: mr,
author: user_exists("John Doe"),
- line_code: sample_commit.line_code,
note: 'Line is wrong')
end
@@ -519,13 +518,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step '"Bug NS-05" has CI status' do
project = merge_request.source_project
project.enable_ci
- ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch
- create :ci_build, commit: ci_commit
+ pipeline = create :ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch
+ create :ci_build, pipeline: pipeline
end
step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do
- expect(page).to have_link "Build pending"
+ expect(page).to have_link "Pipeline: pending"
end
end
@@ -567,7 +566,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_diff_line(sample_compare.changes[1][:line_code])
end
- def have_visible_content (text)
+ def have_visible_content(text)
have_css("*", text: text, visible: true)
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 9b59b682676..019b3124a86 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -20,11 +20,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "master" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "master"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "master" on graph' do
@@ -40,11 +40,19 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
When 'I switch ref to "feature"' do
- select 'feature', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'feature'
+ end
end
When 'I switch ref to "v1.0.0"' do
- select 'v1.0.0', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'v1.0.0'
+ end
end
When 'click "Show only selected branch" checkbox' do
@@ -68,11 +76,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "feature" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "feature"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "feature"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "feature" on graph' do
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index ef185861e00..76fefee9254 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should not see "Snippets" button' do
- expect(page).not_to have_link 'Snippets'
+ page.within '.content' do
+ expect(page).not_to have_link 'Snippets'
+ end
end
step 'project "Shop" belongs to group' do
@@ -123,16 +125,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps
@project.save!
end
- step 'I should see back to dashboard button' do
- expect(page).to have_content 'Go to dashboard'
- end
-
- step 'I should see back to group button' do
- expect(page).to have_content 'Go to group'
- end
-
step 'I click notifications drop down button' do
- click_link 'notifications-button'
+ first('.notifications-btn').click
end
step 'I choose Mention setting' do
@@ -140,8 +134,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
+ page.within '#notifications-button' do
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
index 8c1d09d6cc6..b8da5e6435d 100644
--- a/features/steps/project/project_find_file.rb
+++ b/features/steps/project/project_find_file.rb
@@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
end
step 'I should see "find file" page' do
- ensure_active_main_tab('Files')
+ ensure_active_main_tab('Repository')
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')
+ ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
@@ -66,7 +66,6 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
expect(page).not_to have_content(".gitignore")
end
-
def find_file(text)
fill_in 'file_find', with: text
end
diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb
index 2508c09e36d..1864b3a2b52 100644
--- a/features/steps/project/project_milestone.rb
+++ b/features/steps/project/project_milestone.rb
@@ -52,7 +52,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end
step 'I click link "Labels"' do
- page.within('.nav-links') do
+ page.within('.layout-nav .nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
end
diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb
index 49e9c5520bb..8143b01ca40 100644
--- a/features/steps/project/project_shortcuts.rb
+++ b/features/steps/project/project_shortcuts.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps
include SharedPaths
include SharedProject
include SharedProjectTab
+ include SharedShortcuts
step 'I press "g" and "f"' do
find('body').native.send_key('g')
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index 786a0cad975..beb8ecfc799 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -43,12 +43,12 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
step 'I click link "Edit"' do
page.within ".detail-page-header" do
- click_link "Edit"
+ first(:link, "Edit").click
end
end
step 'I click link "Delete"' do
- click_link "Delete"
+ first(:link, "Delete").click
end
step 'I submit new snippet "Snippet three"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index c26d7a15212..0fe046dcbf6 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -202,8 +202,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Directory »'
- expect(page).not_to have_link 'Browse Code »'
+ expect(page).to have_link 'Browse Directory'
+ expect(page).not_to have_link 'Browse Code'
end
step 'I click on readme file' do
@@ -219,7 +219,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I see Browse code link' do
expect(page).to have_link 'Browse Files'
- expect(page).not_to have_link 'Browse Directory »'
+ expect(page).not_to have_link 'Browse Directory'
end
step 'I click on Permalink' do
@@ -290,15 +290,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step "I switch ref to 'test'" do
- select "'test'", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'test'
+ end
end
step "I switch ref to fix" do
- select "fix", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'fix'
+ end
end
step "I see the ref 'test' has been selected" do
- expect(page).to have_selector '.select2-chosen', text: "'test'"
+ expect(page).to have_selector '.dropdown-toggle-text', text: "'test'"
end
step "I visit the 'test' tree" do
@@ -337,13 +345,15 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see buttons for allowed commands' do
- expect(page).to have_content 'Raw'
- expect(page).to have_content 'History'
- expect(page).to have_content 'Permalink'
- expect(page).not_to have_content 'Edit'
- expect(page).not_to have_content 'Blame'
- expect(page).to have_content 'Delete'
- expect(page).to have_content 'Replace'
+ page.within '.content' do
+ expect(page).to have_content 'Raw'
+ expect(page).to have_content 'History'
+ expect(page).to have_content 'Permalink'
+ expect(page).not_to have_content 'Edit'
+ expect(page).not_to have_content 'Blame'
+ expect(page).to have_content 'Delete'
+ expect(page).to have_content 'Replace'
+ end
end
step 'I should see a notice about a new fork having been created' do
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 3fbcf770b62..f32576d2cb1 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Mike" in team list as "Reporter"' do
- page.within ".access-reporter" do
+ user = User.find_by(name: 'Mike')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
end
end
@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- page.within ".access-reporter" do
+ project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
+ expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Dmitriy" in team list as "Developer"' do
- page.within ".access-developer" do
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
+ expect(page).to have_content('Developer')
end
end
@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Dmitriy" in team list as "Reporter"' do
- page.within ".access-reporter" do
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
+ expect(page).to have_content('Reporter')
end
end
- step 'I click link "Remove from team"' do
- click_link "Remove from team"
- end
-
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name)
@@ -120,13 +126,13 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_link('Remove user from team')
+ click_link('Remove user from project')
end
end
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
- os_group = create(:group, name: 'OpenSource')
+ os_group = create(:group, name: 'OpenSource')
create(:project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 9f6aed1c5b9..3cbf832c728 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -97,7 +97,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
file = Gollum::File.new(wiki.wiki)
Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file)
Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg")
- expect(page).to have_link('image', href: "image.jpg")
+ expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
click_on "image"
end
@@ -113,7 +113,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I click on image link' do
- expect(page).to have_link('image', href: "image.jpg")
+ expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
click_on "image"
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index 0bee91d758d..4eef7aff213 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -2,46 +2,26 @@ module SharedActiveTab
include Spinach::DSL
def ensure_active_main_tab(content)
- expect(find('.nav-sidebar > li.active')).to have_content(content)
+ expect(find('.layout-nav li.active')).to have_content(content)
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.nav-links li.active')).to have_content(content)
+ expect(find('.sub-nav li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
- expect(find('.sidebar-subnav > li.active')).to have_content(content)
+ expect(find('.layout-nav .controls li.active')).to have_content(content)
end
step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
end
step 'no other sub navs should be active' do
- expect(page).to have_selector('.sidebar-subnav > li.active', count: 1)
- end
-
- step 'the active main tab should be Home' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Projects' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Issues' do
- ensure_active_main_tab('Issues')
- end
-
- step 'the active main tab should be Merge Requests' do
- ensure_active_main_tab('Merge Requests')
- end
-
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
+ expect(page).to have_selector('.layout-nav .controls li.active', count: 1)
end
end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index cf30e23b6bd..4d6b258f577 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -10,8 +10,8 @@ module SharedBuilds
end
step 'project has a recent build' do
- @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build_with_coverage, commit: @ci_commit)
+ @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
+ @build = create(:ci_build_with_coverage, pipeline: @pipeline)
end
step 'recent build is successful' do
@@ -23,7 +23,7 @@ module SharedBuilds
end
step 'project has another build that is running' do
- create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running')
+ create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
end
step 'I visit recent build details page' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index e846c52d474..56ef44ec969 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[id$='#{sample_commit.line_code}']") do
+ page.within("form[data-line-code='#{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[id$='#{sample_commit.line_code}']") do
+ page.within("#{diff_file_selector} form[data-line-code='#{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[id$='#{sample_commit.line_code}']") do
+ page.within("#{diff_file_selector} form[data-line-code='#{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[id$='#{sample_commit.line_code}']") do
+ page.within("form[data-line-code='#{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[id$='#{sample_commit.del_line_code}']") do
+ page.within("form[data-line-code='#{sample_commit.del_line_code}']") do
fill_in "note[note]", with: "DRY this up"
find('.js-md-preview-button').click
end
@@ -91,7 +91,7 @@ module SharedDiffNote
page.within(diff_file_selector) do
click_diff_line(sample_commit.line_code)
- page.within("form[id$='#{sample_commit.line_code}']") do
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
fill_in 'note[note]', with: ':smile:'
click_button('Comment')
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index a58b3cb7e16..b5fd24d246f 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -111,7 +111,7 @@ module SharedIssuable
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
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link "Oldest updated"
end
end
@@ -119,7 +119,7 @@ module SharedIssuable
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
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link 'Least popular'
end
end
@@ -127,33 +127,17 @@ module SharedIssuable
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
+ page.within('.content 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
+ page.within('.content 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'
@@ -205,5 +189,4 @@ module SharedIssuable
expect(page).to have_content content
end
end
-
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index a3c3887ab46..3d7c6ef9d2d 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -107,7 +107,7 @@ module SharedNote
end
step 'I should see no notes at all' do
- expect(page).to_not have_css('.note')
+ expect(page).not_to have_css('.note')
end
# Markdown
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index ea5f9580308..0b4920883b8 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -95,7 +95,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Features:")
+ expect(page).to have_content("Features")
end
def current_project
@@ -223,6 +223,11 @@ module SharedProject
create(:label, project: project, title: 'enhancement')
end
+ step 'project "Shop" has issue: "bug report"' do
+ project = Project.find_by(name: "Shop")
+ create(:issue, project: project, title: "bug report")
+ end
+
step 'project "Shop" has CI enabled' do
project = Project.find_by(name: "Shop")
project.enable_ci
@@ -230,7 +235,7 @@ module SharedProject
step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop")
- create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped'
+ create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped'
end
step 'I should see last commit with CI status' do
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index 4fc2ece79ff..d6024212601 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -8,12 +8,8 @@ module SharedProjectTab
ensure_active_main_tab('Project')
end
- step 'the active main tab should be Files' do
- ensure_active_main_tab('Files')
- end
-
- step 'the active main tab should be Commits' do
- ensure_active_main_tab('Commits')
+ step 'the active main tab should be Repository' do
+ ensure_active_main_tab('Repository')
end
step 'the active main tab should be Graphs' do
@@ -41,9 +37,7 @@ module SharedProjectTab
end
step 'the active main tab should be Settings' do
- page.within '.nav-sidebar' do
- expect(page).to have_content('Go to project')
- end
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0)
end
step 'the active main tab should be Activity' do
@@ -53,4 +47,12 @@ module SharedProjectTab
step 'the active sub tab should be Network' do
ensure_active_sub_tab('Network')
end
+
+ step 'the active sub tab should be Files' do
+ ensure_active_sub_tab('Files')
+ end
+
+ step 'the active sub tab should be Commits' do
+ ensure_active_sub_tab('Commits')
+ end
end
diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb
index bbb7afec0ad..a75a8474d26 100644
--- a/features/steps/shared/shortcuts.rb
+++ b/features/steps/shared/shortcuts.rb
@@ -1,4 +1,4 @@
-module SharedActiveTab
+module SharedShortcuts
include Spinach::DSL
step 'I press "g" and "p"' do
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
new file mode 100644
index 00000000000..5c47238777f
--- /dev/null
+++ b/features/steps/shared/sidebar_active_tab.rb
@@ -0,0 +1,35 @@
+module SharedSidebarActiveTab
+ include Spinach::DSL
+
+ step 'the active main tab should be Help' do
+ ensure_active_main_tab('Help')
+ end
+
+ step 'no other main tabs should be active' do
+ expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+ end
+
+ def ensure_active_main_tab(content)
+ expect(find('.nav-sidebar li.active')).to have_content(content)
+ end
+
+ step 'the active main tab should be Home' do
+ ensure_active_main_tab('Projects')
+ end
+
+ step 'the active main tab should be Projects' do
+ ensure_active_main_tab('Projects')
+ end
+
+ step 'the active main tab should be Issues' do
+ ensure_active_main_tab('Issues')
+ end
+
+ step 'the active main tab should be Merge Requests' do
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ step 'the active main tab should be Help' do
+ ensure_active_main_tab('Help')
+ end
+end
diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb
index cf999879579..32e29ffad1e 100644
--- a/features/steps/snippet_search.rb
+++ b/features/steps/snippet_search.rb
@@ -52,5 +52,4 @@ class Spinach::Features::SnippetSearch < Spinach::FeatureSteps
step 'I should not see "Personal snippet private" in results' do
expect(page).not_to have_content 'Personal snippet private'
end
-
end
diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb
index 023032e679f..19366b11071 100644
--- a/features/steps/snippets/snippets.rb
+++ b/features/steps/snippets/snippets.rb
@@ -14,12 +14,12 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
step 'I click link "Edit"' do
page.within ".detail-page-header" do
- click_link "Edit"
+ first(:link, "Edit").click
end
end
step 'I click link "Delete"' do
- click_link "Delete"
+ first(:link, "Delete").click
end
step 'I submit new snippet "Personal snippet three"' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
index b1d088f07f9..59385a6ab59 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -34,7 +34,7 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
step 'I should see contributions calendar' do
- expect(page).to have_css('.cal-heatmap-container')
+ expect(page).to have_css('.js-contrib-calendar')
end
def contributed_project
diff --git a/features/support/env.rb b/features/support/env.rb
index 357d164d87f..ab3f0ca7aeb 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -2,11 +2,6 @@ if ENV['SIMPLECOV']
require 'simplecov'
end
-if ENV['COVERALLS']
- require 'coveralls'
- Coveralls.wear_merged!
-end
-
ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec/expectations'
@@ -16,6 +11,11 @@ require_relative 'capybara'
require_relative 'db_cleaner'
require_relative 'rerun'
+if ENV['CI']
+ require 'knapsack'
+ Knapsack::Adapters::RSpecAdapter.bind
+end
+
%w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 41ca617847e..50ee5089d8f 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -2,62 +2,62 @@
{
"name": "100",
"unicode": "1F4AF",
- "digest": "6d57c7cc93335f853e1a5670233f121bc94730dbd82b2b3c5c5a509e092ef0fd"
+ "digest": "add3bd7d06b6dd445788b277f8c9e5dcf42a54d3ec8b7fb9e7a39695dd95d094"
},
{
"name": "1234",
"unicode": "1F522",
- "digest": "727763fd9f18fd5df59e9f78e678ea4ec753e674d70f15d4e77c7802067d660b"
+ "digest": "c5ac5c8147f5bfd644fad6b470432bba86ffc7bcee04a0e0d277cd1ca485207f"
},
{
"name": "8ball",
"unicode": "1F3B1",
- "digest": "1aecf21951452ba24e921ec71b3d313b7ddc2e185b0339c9e0eebc85be4f031d"
+ "digest": "a6e6855775b66c505adee65926a264103ebddf2e2d963db7c009b4fec3a24178"
},
{
"name": "a",
"unicode": "1F170",
- "digest": "2272113a5bcb7faf8db7c1bd35df576d32f2f7cbd881463934ad3382eb87c723"
+ "digest": "bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc"
},
{
"name": "ab",
"unicode": "1F18E",
- "digest": "6f8a237751fdc84db4121f408272d9a23258515449610e4c6c54f50f6e995627"
+ "digest": "67430fe5fce981160e2ea9052962e49f264322d3abfc2828fbc311b6cdf67ae8"
},
{
"name": "abc",
"unicode": "1F524",
- "digest": "652a2381a7b587d8a52d5178e2d7d6c8600b33d36160fa69677943da374105bc"
+ "digest": "282c817ee3414d77a74b815962c33dd9fe71fabaea8c7a9cec466100fbe32187"
},
{
"name": "abcd",
"unicode": "1F521",
- "digest": "35ade4fd3d75294ebb72c24490aa32745604edc6cabe095b90634cd3ce78c07b"
+ "digest": "686728c759f4683c64762ee4eda0a91bf2041f0ae4f358aacf6c09bf51892eff"
},
{
"name": "accept",
"unicode": "1F251",
- "digest": "8212ed158cc447c92813273fc915e84d3d5c4c48d1b38e498c088bad27ab8145"
+ "digest": "7208d34c761f10a7fd28f98e25535eba13ff91a64442fc282a98bb77722614f1"
},
{
"name": "aerial_tramway",
"unicode": "1F6A1",
- "digest": "8039d7f67e6e5b211066cab6cf2142afc3aca5c830a357369362c9b484029563"
+ "digest": "98df666f34370fc34ce280d84bba5a7e617f733fbbfe66caa424b2afa6ab6777"
},
{
"name": "airplane",
"unicode": "2708",
- "digest": "18f4dfac323555d8cdabb79148874c0185ce98e1a08e69414d236b23e502a854"
+ "digest": "cc12cf259ef88e57717620cd2bd5aa6a02a8631ee532a3bde24bee78edc5de33"
},
{
"name": "airplane_arriving",
"unicode": "1F6EC",
- "digest": "9a1c81d97512e5d0e3acec40290d00f616ec182140909859e366a734b9f840bb"
+ "digest": "80d5b4675f91c4cff06d146d795a065b0ce2a74557df4d9e3314e3d3b5c4ae82"
},
{
"name": "airplane_departure",
"unicode": "1F6EB",
- "digest": "e3c5ff4038db998c1897cb237d0b865da0bc60331c758f204e45a979d5fab445"
+ "digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332"
},
{
"name": "airplane_northeast",
@@ -72,12 +72,12 @@
{
"name": "airplane_small",
"unicode": "1F6E9",
- "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3"
+ "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
},
{
"name": "small_airplane",
"unicode": "1F6E9",
- "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3"
+ "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
},
{
"name": "airplane_small_up",
@@ -102,67 +102,67 @@
{
"name": "alarm_clock",
"unicode": "23F0",
- "digest": "84ddd7b3b857c165410b7b44863e5354ca0f3591c3bfe56231f12c9f7531a96f"
+ "digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599"
},
{
"name": "alembic",
"unicode": "2697",
- "digest": "45698914a21683f06931d807af171bcb6984e5ebce66012bba71b467565bd69d"
+ "digest": "c94b2a4bf24ccf4db27a22c9725cfe900f4a99ec49ef2411d67952bcb2ca1bfb"
},
{
"name": "alien",
"unicode": "1F47D",
- "digest": "94dbe4e90614c654145aba93610c43e3ab86df8ca07391bd4e56383f9329c008"
+ "digest": "856ba98202b244c13a5ee3014a6f7ad592d8c119a30d79e4fc790b74b0e321f7"
},
{
"name": "ambulance",
"unicode": "1F691",
- "digest": "82ef36bcd13c88a4b2397c918b8048adc6bf045ed2532ff568e0dfd1b1b29c3c"
+ "digest": "d9b3c1873de496a4554e715342c72290fb69a9c6766d7885f38bfe9491d052da"
},
{
"name": "amphora",
"unicode": "1F3FA",
- "digest": "d3758d88aa1fc3be01894102f57479d3a49790510d38ad3d06a2774962010608"
+ "digest": "4015f907b649b5e348502cc0e3685ed184e180dca5cc81c43ec516e14df127bf"
},
{
"name": "anchor",
"unicode": "2693",
- "digest": "27c6034f769d9f020362fc5b227b9279651cc940861e727d1f6ccd59af98f851"
+ "digest": "2b29b34ef896ebab70016301e3d1880209bbc3c5a5b8d832e43afff9b17ad792"
},
{
"name": "angel",
"unicode": "1F47C",
- "digest": "c1b8ad2adc7686e7fbbe4ec357071e7228a5e0762e001bb589e2f97ff258d5c7"
+ "digest": "db75c2460aaf9cd07cb41fe22c8a6079f3667ffe612a71611358720e2b5512a4"
},
{
"name": "angel_tone1",
"unicode": "1F47C-1F3FB",
- "digest": "90b701c43311b1096c4a012d9905a186f1a16829ea2707921a8418c28617d751"
+ "digest": "5871a622469b96296365adaf77d83167759692124c20e5a6e062a525af33472a"
},
{
"name": "angel_tone2",
"unicode": "1F47C-1F3FC",
- "digest": "d6bcaf1b76e25d486d4ab9b159cf727782d508543d1ae27c8d2c12d2f13d6eb0"
+ "digest": "f5993198a5d9daf39e761c783461f07bca237f4e9b739ac300bb8ca001a69a1a"
},
{
"name": "angel_tone3",
"unicode": "1F47C-1F3FD",
- "digest": "3069285e6218c8083cb0085aa10017bcdea033e321d97ba339a84892074b903a"
+ "digest": "f0c97a7c4354626267d6ab0f388e4297ad255ab9b061f9c68fbcaa0abfc52783"
},
{
"name": "angel_tone4",
"unicode": "1F47C-1F3FE",
- "digest": "dbb87019752d9caa94ce086858c1e3225b62e221ad599f5106548fda2456fc2b"
+ "digest": "6e5dc724c1939d1b0d1a91343662b5bd61ced7709c97802977145ffab6a1f7ac"
},
{
"name": "angel_tone5",
"unicode": "1F47C-1F3FF",
- "digest": "f77703df97720c27a128b5f3c0948b9e04a6b6b81ea5306468154f9bf56225db"
+ "digest": "52186e1de350c27d25d6010edf44f64a30338b65912ca178429fbcfbd88113c2"
},
{
"name": "anger",
"unicode": "1F4A2",
- "digest": "2253b7ff0894f247bc6f04d841a748c56d6c94684880c13df42387691ff20e75"
+ "digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f"
},
{
"name": "anger_left",
@@ -177,152 +177,152 @@
{
"name": "anger_right",
"unicode": "1F5EF",
- "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4"
+ "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
},
{
"name": "right_anger_bubble",
"unicode": "1F5EF",
- "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4"
+ "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
},
{
"name": "angry",
"unicode": "1F620",
- "digest": "c4188ba70df99d8ccef5706d711176725d3dd50d62f065a177d68d85c7828107"
+ "digest": "7e09e7e821f511606341fb5ce4011a8ed9809766ab86b7983ffa6ea352b39ec1"
},
{
"name": "anguished",
"unicode": "1F627",
- "digest": "9c2347308133ae50dc04da62042fff847f4c477b2956b8aa976f0413899e38bc"
+ "digest": "a2b6f052996969a17150249d9ef5db742da3d6585bd38ca61eb14c4c13cda54f"
},
{
"name": "ant",
"unicode": "1F41C",
- "digest": "d2af2ed1cfe15d649aa329d965764a1e8726941d833841781a5b66d7dd0b0921"
+ "digest": "929abeaff7ba21ab71cd1ab798af7a6b611e3b3ce1af80cede09a116b223e442"
},
{
"name": "apple",
"unicode": "1F34E",
- "digest": "a9babee24f454934a5e1fb8d781cbce354dfd88e8a8e01f02e8b30071fd40460"
+ "digest": "2a1b85ce57e3d236ae7777dcf332ec37d03bfd7b19806521a353bc532083224d"
},
{
"name": "aquarius",
"unicode": "2652",
- "digest": "1a168c252678847d1f9ef450887489e3bdc207ecae4b6fb05e92295ff861ae2c"
+ "digest": "fdc42cd41b0dace5eae6baba3143f1e40295d48a29e7103a5bba1d84a056c39d"
},
{
"name": "aries",
"unicode": "2648",
- "digest": "bde262a8795e12f8b0ebb3f0f8c3a56104062fcee8d5d678cf4bb445a7daf698"
+ "digest": "deb135debcde0a98f40361a84ab64d57c18b5b445cd2f4199e8936f052899737"
},
{
"name": "arrow_backward",
"unicode": "25C0",
- "digest": "ddae36d1febf5c246e51d599e2898a8aa30cd47f88b5bcb469e3ca9d22538b97"
+ "digest": "e162ac82e90d1e925d479fa5c45b9340e0a53287be04e43cbbb2a89c7e7e45e4"
},
{
"name": "arrow_double_down",
"unicode": "23EC",
- "digest": "906f42b5f788128ed90d2d162cf03e6e595a50ad05e0aa5f64e925637379d0cd"
+ "digest": "03ca890b05338d40972c7a056d672df620a203c6ca52ff3ff530f1a710905507"
},
{
"name": "arrow_double_up",
"unicode": "23EB",
- "digest": "2129a57402980de6fc6f59ad8354525c2dbcd66d1b78f4de091181ddc81e0693"
+ "digest": "e753f05bce993d62d5dc79e33c441ced059381b6ce21fa3ea4200f1b3236e59d"
},
{
"name": "arrow_down",
"unicode": "2B07",
- "digest": "370e4f41565d5dab245c20e45c502505a56d26c2392283781b841eb3e905edb2"
+ "digest": "9bf1bd2ea652ca9321087de58c7a112ea04c35676a6ee0766154183f8b95af6c"
},
{
"name": "arrow_down_small",
"unicode": "1F53D",
- "digest": "98a2b183f2daec425160bbfce1d2b940b8baa0d5032fdacfa9453e39bed5651b"
+ "digest": "7766198bc60cf59d6cdaeeaa700c2282bfff2f0fdeb22cf4581ca284b87a3bb7"
},
{
"name": "arrow_forward",
"unicode": "25B6",
- "digest": "348627b8e0f55cf1e9ab19c9de1d170371b2c4cb4dda9a2aa8e0c558db08b18a"
+ "digest": "db77d9accd1e02224f5d612f79cd691e6befdf22063475204836be6572510fb7"
},
{
"name": "arrow_heading_down",
"unicode": "2935",
- "digest": "96c64953fc3134711247bef320f252c48993ebc90494925b7fee42ffce2a2ec2"
+ "digest": "f5396069c8f63c13e6c3e0ecd34267c932451309ade9c1171d410563153bf909"
},
{
"name": "arrow_heading_up",
"unicode": "2934",
- "digest": "94f94e74176cc050703b3584f3f700debf86e4e61b893a441825a21fa3f8ce74"
+ "digest": "1cad71923fa3df24cf543cae4ce775b0f74936f2edd685fd86a7525c41a14568"
},
{
"name": "arrow_left",
"unicode": "2B05",
- "digest": "4553be62a63d7550deac4f7dbeffce6006f769ae6cddfb8c795671672011ba0b"
+ "digest": "b629bb3dbe161ef89cfcfced0c7968a68e44a019ad509132987e4973bdc874e7"
},
{
"name": "arrow_lower_left",
"unicode": "2199",
- "digest": "10f83c252110d705cdcfebc35a70c341ad288730d0c0729479e3a96e263d5120"
+ "digest": "879136ba0e24e6bf3be70118abcb716d71bd74f7b62347bc052b6533c0ea534d"
},
{
"name": "arrow_lower_right",
"unicode": "2198",
- "digest": "ee33abd4c96c19e9b80a2fc1500ba8ecaa6668c49310cc816a496e8c61af3850"
+ "digest": "86d52ac9b961991e3aaa6a9f9b5ace4db6ffd1b5c171c09c23b516473b55066d"
},
{
"name": "arrow_right",
"unicode": "27A1",
- "digest": "2611e9138a2651916f414015d0287f5f0af266514d96a42915d32b04fb652a90"
+ "digest": "45f26a1cbb0f00ed3609b39da52e9d9e896a77e361c4c8036b1bf8038171bd49"
},
{
"name": "arrow_right_hook",
"unicode": "21AA",
- "digest": "628b06384a2963a4fe81e9fbf4e22511f697878d9b9db7d2fc98f8aadbe8f4f9"
+ "digest": "4f452679c71bcea4fc4a701c55156fef3ddc1ebbc70570bedfc9d3a029637ab1"
},
{
"name": "arrow_up",
"unicode": "2B06",
- "digest": "c09e5f41c01028b45707c525d30d3d6731ec57b7447f0d7ba4ad6c1404449e5c"
+ "digest": "982b988ef6651d8a71867ba7c87f640f62dd0eeb0b7c358f5a5c37e8fe507b8b"
},
{
"name": "arrow_up_down",
"unicode": "2195",
- "digest": "e7fd92d24a01702f76c7fcc0de998bc81fbfb93711d076984f6da91d1dccd84c"
+ "digest": "645ed8fb6646f49bfd95af1752336deacdadbe5cba13904023a704288f3b0e2c"
},
{
"name": "arrow_up_small",
"unicode": "1F53C",
- "digest": "bc48dad74bc1d0c5579cbf5e3d005314b0d21bc5b5ebbba2b05136e33f49296d"
+ "digest": "4a8c5789c13a852517e639e7a62c2d331464e6fb0358985aa97c1515e97b5e8b"
},
{
"name": "arrow_upper_left",
"unicode": "2196",
- "digest": "792a9709f03843024e53d201cb4769c59b656c3bf0dff2306e8e605493a66b93"
+ "digest": "79026f828d6ceb7c55a9542770962ba6dcd08203995f6ceeb70333a12307d376"
},
{
"name": "arrow_upper_right",
"unicode": "2197",
- "digest": "ee934b0c9cff270efd30a6cafc15253d405efd2c93b4785ac2ed4ea6420266a6"
+ "digest": "7e0f33dfbe65628991c170130d366a3e2cedaf8862ddfcaf3960f395d3da1926"
},
{
"name": "arrows_clockwise",
"unicode": "1F503",
- "digest": "914f4120513730d7a19c9f8c4e59223a90568de0b25a225b712b31fa9697ef4f"
+ "digest": "88669679977f7157f0acaa9d6a1b77ccf84d25eb78c5bc8afcde38d3635e7144"
},
{
"name": "arrows_counterclockwise",
"unicode": "1F504",
- "digest": "86d87597e4e3db6dbba9907ee82412db0cbab1ea875bd0be6505dd886dc19b90"
+ "digest": "a2c6a6d3643c128aee3304cd03bb3d7cfe4d35d3ba825bc9c1142d7832b4426e"
},
{
"name": "art",
"unicode": "1F3A8",
- "digest": "dfc6b0da780199df86507d65b0499ba1706c266ae7badcb0e7fb5b85af7c9578"
+ "digest": "b6bc6c4bfb594aadcbb641d006031867678504764bbe0ab84e7b08567a9498da"
},
{
"name": "articulated_lorry",
"unicode": "1F69B",
- "digest": "4c4de240ebd175f7b53453eda4e51f2e57d0db2a98d317f804116e14e47cff1d"
+ "digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa"
},
{
"name": "ascending_notes",
@@ -332,117 +332,117 @@
{
"name": "asterisk",
"unicode": "002A-20E3",
- "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f"
+ "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
},
{
"name": "keycap_asterisk",
"unicode": "002A-20E3",
- "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f"
+ "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
},
{
"name": "astonished",
"unicode": "1F632",
- "digest": "58632b97e274ade5183752db2b3c5c4fe29effcd5a9720a8d01fa809b97023dc"
+ "digest": "f8531bdda5070d10492709085f4ff652b8be9be6458758940358b9fc594a1f14"
},
{
"name": "athletic_shoe",
"unicode": "1F45F",
- "digest": "1fc55d85a4d6751f9e60467801b051d2fb3341bdcc33b8d3695d5143359edb43"
+ "digest": "1f90dc390e0dea679085465b7f9e786dfd7dd56a3b219987144ed37ab1e9bf95"
},
{
"name": "atm",
"unicode": "1F3E7",
- "digest": "bf827ef6c349f5b6912d821457975a4720d1750529d907e94ece429b7a388d7e"
+ "digest": "7d3ce6a6afb4951546883404b8e36904179f88f1aa533706cf7bf0bbe0d6fd3c"
},
{
"name": "atom",
"unicode": "269B",
- "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669"
+ "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
},
{
"name": "atom_symbol",
"unicode": "269B",
- "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669"
+ "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
},
{
"name": "b",
"unicode": "1F171",
- "digest": "9116256b3189977e37f6da7ddedf82bb29b0358829a4e8718fd59e51d9b86b3c"
+ "digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf"
},
{
"name": "baby",
"unicode": "1F476",
- "digest": "66596bea11015154e0b1752b85f349f4286c6643ee6f51ee5e60e0d625c4ae9a"
+ "digest": "219ae5a571aaf90c060956cd1c56dcc27708c827cecdca3ba1122058a3c4847b"
},
{
"name": "baby_bottle",
"unicode": "1F37C",
- "digest": "ed42994b4a539b8bfeccde0f3c7e9c7f54d6696ff48ce7e48171bbab51002348"
+ "digest": "4fb71689e9d634e8d1699cf454a71e43f2b5b1a5dbab0bf186626934fdf5b782"
},
{
"name": "baby_chick",
"unicode": "1F424",
- "digest": "ea2cfa0e5c2cbff5fffdb52cc04dfe7872834bd7cfeaa45e0541b8faffcbd0e9"
+ "digest": "14119874e9b5548028dfb9cc593a541efc1d075ac839a565b92e0c3253cffe7e"
},
{
"name": "baby_symbol",
"unicode": "1F6BC",
- "digest": "65df04dff8739b86f7663ae9c0648927341f360a986655e109721b0e16013b75"
+ "digest": "fb4db66868cda45ea3879ffc2ff4f763c56d2d889ae0ab17fe171129ede02f98"
},
{
"name": "baby_tone1",
"unicode": "1F476-1F3FB",
- "digest": "bc747527a2d723cf99ef3fc2539c19d29634c92ff417736982d3bf87d65d06eb"
+ "digest": "cd3faf223a298c34e05d469d9d0db08438d97df7fd82c0973f8a9e07d553f5b1"
},
{
"name": "baby_tone2",
"unicode": "1F476-1F3FC",
- "digest": "b82bba7a666b7d070751726e54acc7fb8f96e2dfc09e9610d61cfd20947aef9c"
+ "digest": "5b4539e22e0dd726c27eb8af2357f9240a52aed3f710f3234571cff029cc6198"
},
{
"name": "baby_tone3",
"unicode": "1F476-1F3FD",
- "digest": "7f45dfd4ea2ae8515d419ffa13e7ee5c625b024b4e521ace5344c414bb929da0"
+ "digest": "720e740e1ac63c6372269132b1fb6e07a6b91f5c808cc3adef59f0b4500e5e72"
},
{
"name": "baby_tone4",
"unicode": "1F476-1F3FE",
- "digest": "80b1854626616f15426649cc6415e4911a55c8f761422fe48a08af9e8ac6a7cb"
+ "digest": "5e43b69c509bd526ad6f081764578c30b6f3285fb7442222e05ccf62e53bfb64"
},
{
"name": "baby_tone5",
"unicode": "1F476-1F3FF",
- "digest": "9f890804d19a61bee76a29644c818045dd96cf69d67cfbca2d11f4ad376b27da"
+ "digest": "85bba6e0940ccfb99999fe124e815f9dd340d00a5568e13967b02245a62dbf54"
},
{
"name": "back",
"unicode": "1F519",
- "digest": "1dc73947b8f56e033777ca3f747407923bd16b07e53a6c78b09950ca474b7e7a"
+ "digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e"
},
{
"name": "badminton",
"unicode": "1F3F8",
- "digest": "3f95180c1175d0248ebf4b8650cf86566c39e0486d828078244080194c14d4fe"
+ "digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66"
},
{
"name": "baggage_claim",
"unicode": "1F6C4",
- "digest": "7c1a69511aa2a93984d601da4d1cef1cb4cefbbf127b1486278da8c01345bbf3"
+ "digest": "7d6bceca92c266da6d2b91dfcf244546fc11022e039e7da8e6888c1696bb2186"
},
{
"name": "balloon",
"unicode": "1F388",
- "digest": "a10c2b0865179cdbdef339494ec9b2a109451a356e53738d6a9dd43232500956"
+ "digest": "65760aedc1503b426927cff78c24449d563843a274961d962718fa9638375d54"
},
{
"name": "ballot_box",
"unicode": "1F5F3",
- "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a"
+ "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
},
{
"name": "ballot_box_with_ballot",
"unicode": "1F5F3",
- "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a"
+ "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
},
{
"name": "ballot_box_check",
@@ -457,7 +457,7 @@
{
"name": "ballot_box_with_check",
"unicode": "2611",
- "digest": "5f5cec7fe462557d31e8d2b836534c1e76d546cc0061236fa2af3667972b84aa"
+ "digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134"
},
{
"name": "ballot_box_x",
@@ -482,277 +482,277 @@
{
"name": "bamboo",
"unicode": "1F38D",
- "digest": "feb0cf2f1012a1c0649b8c66f7e96e2d8bcdefe879c5a52dab3e25c51009e3b2"
+ "digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd"
},
{
"name": "banana",
"unicode": "1F34C",
- "digest": "aa9a1e6db00efa94a7f414c570eff7fc29011be64031a24d03b7f37b617cfd2d"
+ "digest": "f9e8ff910c282c20a8907ff64926b5de4ee250529a1ed718fb33302e6fff8dd9"
},
{
"name": "bangbang",
"unicode": "203C",
- "digest": "bdd350766ccd1c0138f6294f7ebfa3e9867b02bda40a743f7062e52c68358765"
+ "digest": "76536fee63fe964a3f3839d309b1f45028fb0c43f4d1eeee495f17e1532b4def"
},
{
"name": "bank",
"unicode": "1F3E6",
- "digest": "c9648c93049cf8e7884242e58ae3145383d2e5034c9090e0d34c53f5bbce397f"
+ "digest": "f5d2976bf6d521638ccacc74be06bd4abfeab06c5d898a9d245edad45a5b6306"
},
{
"name": "bar_chart",
"unicode": "1F4CA",
- "digest": "942277f72a5b754b13454dab62c85b1ff3447544f38ec76a285f3be32f6f5d12"
+ "digest": "65a328a1b2d7a5332dd4d93f4dbca13d976f0a505b00835c3fc458e394804240"
},
{
"name": "barber",
"unicode": "1F488",
- "digest": "e1526eea685aafc56fb83d07f8ff63c9967600e447b0e5f831a17d6153f2062d"
+ "digest": "5e8053d3bb3765a8632fd1cbfe21163f74ed79f6be377eb9603eaaf883d8dc46"
},
{
"name": "baseball",
"unicode": "26BE",
- "digest": "3d028b16a898f3a15874bc9d3891f9fbf59ea1c226c5c774eddb58a712c489ae"
+ "digest": "46ac16f8b5455b942f6dbff9483a6fd277721e6719d2731573baabd21c44b34f"
},
{
"name": "basketball",
"unicode": "1F3C0",
- "digest": "b2f5a3904d505db066337a24fc840ef75b49ef4c5f152227d8e632ff82285b12"
+ "digest": "cc83e2aea8fcd2e9a5789e1932ee3766c40843c142fd3565c4e77dafb21ec7d7"
},
{
"name": "basketball_player",
"unicode": "26F9",
- "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e"
+ "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
},
{
"name": "person_with_ball",
"unicode": "26F9",
- "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e"
+ "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
},
{
"name": "basketball_player_tone1",
"unicode": "26F9-1F3FB",
- "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90"
+ "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
},
{
"name": "person_with_ball_tone1",
"unicode": "26F9-1F3FB",
- "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90"
+ "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
},
{
"name": "basketball_player_tone2",
"unicode": "26F9-1F3FC",
- "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc"
+ "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
},
{
"name": "person_with_ball_tone2",
"unicode": "26F9-1F3FC",
- "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc"
+ "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
},
{
"name": "basketball_player_tone3",
"unicode": "26F9-1F3FD",
- "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf"
+ "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
},
{
"name": "person_with_ball_tone3",
"unicode": "26F9-1F3FD",
- "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf"
+ "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
},
{
"name": "basketball_player_tone4",
"unicode": "26F9-1F3FE",
- "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8"
+ "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
},
{
"name": "person_with_ball_tone4",
"unicode": "26F9-1F3FE",
- "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8"
+ "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
},
{
"name": "basketball_player_tone5",
"unicode": "26F9-1F3FF",
- "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7"
+ "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
},
{
"name": "person_with_ball_tone5",
"unicode": "26F9-1F3FF",
- "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7"
+ "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
},
{
"name": "bath",
"unicode": "1F6C0",
- "digest": "ae6301a6354630cd9dc06a5137f23f826d019c8298b2b012b6ff31b773a910b6"
+ "digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917"
},
{
"name": "bath_tone1",
"unicode": "1F6C0-1F3FB",
- "digest": "fce7ae2e7ef3f7f44f36c2ad49348b4cf7fce0b0c17e1a90a1e85734cee95b2a"
+ "digest": "7ae2989e47788ba71359d52da68feec95aaff68a77d5a6556957df1617af8536"
},
{
"name": "bath_tone2",
"unicode": "1F6C0-1F3FC",
- "digest": "4d1c9444f16467488fe939fdad279d6855d28be564e5dcc1990451c4b9ae8c95"
+ "digest": "2e86f8edad54d15a7094cd52160cbe51d10aa1750cfb0b3b58e93533f070e327"
},
{
"name": "bath_tone3",
"unicode": "1F6C0-1F3FD",
- "digest": "9a59a4360effb48af4cbb1a953655ef61e69375407038b4d0bd8068fbaf3cc16"
+ "digest": "654c0cd083a67ff330a38d07352876d265390e5399e5352598d64a6c7e5eeba7"
},
{
"name": "bath_tone4",
"unicode": "1F6C0-1F3FE",
- "digest": "01aafa8a53a08018b9fbf28ec6b3b918d6bd0dee7a891196f32f81f60d114f0e"
+ "digest": "adad88c6830f31c4b5be194d1987d6aadf4adf45e4cb7f2e4657f0d20c0d663a"
},
{
"name": "bath_tone5",
"unicode": "1F6C0-1F3FF",
- "digest": "2733e81ccaee21231c2e47e3310b431e9bd784bf34f0db609f8eadcee359500d"
+ "digest": "952c4c9bf24e001e23a33ebf97bd92969cd9143e28ce93f9aafc708a8f966903"
},
{
"name": "bathtub",
"unicode": "1F6C1",
- "digest": "9515e3bb9ab41350305e64fc6877aae82d51e1ba8ce8b2b4b8ffaeda960820cd"
+ "digest": "844dffb87ef872594195069b0d0df27c3fe51f3967ccbc8b2df811a086dd483a"
},
{
"name": "battery",
"unicode": "1F50B",
- "digest": "7d4d475c1d5b1be55c319953e3363ff864fe4fcd921a8aa649b9a547c0894deb"
+ "digest": "949ae06648667fb13d9121a6dfdd03bf8692794b28c36e9a8e8ac4515664449a"
},
{
"name": "beach",
"unicode": "1F3D6",
- "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099"
+ "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
},
{
"name": "beach_with_umbrella",
"unicode": "1F3D6",
- "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099"
+ "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
},
{
"name": "beach_umbrella",
"unicode": "26F1",
- "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661"
+ "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
},
{
"name": "umbrella_on_ground",
"unicode": "26F1",
- "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661"
+ "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
},
{
"name": "bear",
"unicode": "1F43B",
- "digest": "b5ac126875c20c82b9e3140b143233944a2e4132d781d0b575e83673988523cb"
+ "digest": "a4b9066eaa5681e6af06e596a96a5217037460ffc3b013e8db4d34d762413246"
},
{
"name": "bed",
"unicode": "1F6CF",
- "digest": "1919245d7a76799aad0533eb72db2cbaa1f32ee8231a0c1989d3f233f2d42370"
+ "digest": "08f6e20db51b1fb650b390a0a3074938646772f3fcee8c295d47742e44fe1e30"
},
{
"name": "bee",
"unicode": "1F41D",
- "digest": "69ada63403c8dabae39c63ba143143aeb59b66faae6aa82d8342337925a9e6b5"
+ "digest": "5beb9a1650681b4adf69999d4808231c38f41a3ec693480b807cda86f964c570"
},
{
"name": "beer",
"unicode": "1F37A",
- "digest": "b71dd6efdb4ce7d9d71fdbf82a2ccf83841fb0cceb119ee7da1e575d3bfa853c"
+ "digest": "69e227104976548ee0f37375fe1526fd65ef0a328d2d92db2feb1edfd7032bd4"
},
{
"name": "beers",
"unicode": "1F37B",
- "digest": "994108cebfe0c614c05967af4e3864d8adbbfcf7cccef1cbd42a47b7dfabf80c"
+ "digest": "db8b32d93bf6d161a3b027e55651d8f51231b13928b3610987ef62bb634d7501"
},
{
"name": "beetle",
"unicode": "1F41E",
- "digest": "ec351ce238a81711eef00e5be1de2e198423cf524b60e531d435902b44420edc"
+ "digest": "5aaa428e3f63f7cd1696839ab05be03fa0cd0cbed30a05c36cb270da330c3849"
},
{
"name": "beginner",
"unicode": "1F530",
- "digest": "13288d9fc221dc02f4181b998104e13c3c5c98d3c4e650186bef59a46d39f6f0"
+ "digest": "2de4fdf92f182c42b12b7527034eaf767d996848b61f31ee69167728411ca0b1"
},
{
"name": "bell",
"unicode": "1F514",
- "digest": "784b9a82814ce14a264e54b3a8f8e706f3c7b763646d9f8174c4aa84ad41ef09"
+ "digest": "18d419417746ead408072b78fe2edb6314cdb49492873966fa9f9f06be09899b"
},
{
"name": "bellhop",
"unicode": "1F6CE",
- "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259"
+ "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
},
{
"name": "bellhop_bell",
"unicode": "1F6CE",
- "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259"
+ "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
},
{
"name": "bento",
"unicode": "1F371",
- "digest": "d59314b17a8646d4a78fefb7b79f289f33d4aaea893fed4cad0b890df63395e7"
+ "digest": "d46d4f681c5da7f7678b51be3445454a8ed18d917e132ae79077f05310e485f1"
},
{
"name": "bicyclist",
"unicode": "1F6B4",
- "digest": "e7359d615d40325bb08a145cfebde2ecef448deeb21695a34b55d3ccb971447f"
+ "digest": "3302147b6b47c16adb97d78b7b761a1ca80e6d0b41d0b60f4da338d2f55f968b"
},
{
"name": "bicyclist_tone1",
"unicode": "1F6B4-1F3FB",
- "digest": "e45808faa32f4ffb881d3569c0b8e2c69d4a64665f4d1fae24d7a1e5f1d3ea4b"
+ "digest": "27eaae0eb61f5e7b3cd9faf02c042d6643a368051a7c9d7da4e0fb9802d39242"
},
{
"name": "bicyclist_tone2",
"unicode": "1F6B4-1F3FC",
- "digest": "92a3494270d1da6a117e92402c7898d4a7fffbe3d6143fb9ae445c4827c0c8a4"
+ "digest": "39ee9e1071700da7079ad0146bf5711c3a222991eeca8b29b72a65677604444d"
},
{
"name": "bicyclist_tone3",
"unicode": "1F6B4-1F3FD",
- "digest": "6fdf1db2bbd08d06b643b08f0f29daeaa20e0b8c8abec21132191f435cc05e42"
+ "digest": "03e1d2c4232c896147a9d4bf43becd61edbb5c84fc7193ecea474c0f9fb36817"
},
{
"name": "bicyclist_tone4",
"unicode": "1F6B4-1F3FE",
- "digest": "d9c27848e1bcc8197c858e1ef12a537f4ed6c77fb211b6731388dc88c2bb7a61"
+ "digest": "61393d9c4805be0379d86dd5bec9a1b02314433ab36cfd85bb48dfd073746617"
},
{
"name": "bicyclist_tone5",
"unicode": "1F6B4-1F3FF",
- "digest": "4892af1a8a0229a813d7b8e3d88481c2365e3e1a5ce2e0e27ce432c5336da810"
+ "digest": "2b46d5f8303e5710dbf5db3a4edc9d88a032fe123fe79158024c9f51df5458c6"
},
{
"name": "bike",
"unicode": "1F6B2",
- "digest": "e726f97b5432f46ed51328c0930d1d63b3a2d7b67c5c2303a5ca997083cfcac1"
+ "digest": "b41daa7c549d483e2336186a28baaa8ecb11986f490c0c54c793c44900c8f652"
},
{
"name": "bikini",
"unicode": "1F459",
- "digest": "7612fcb72c005ae7172260825f588d6995f2bc919cb3d283dd4591f6872a1855"
+ "digest": "07fe156f64673818d69ce3bf03950ca59e3b5d346e45ca541da4078ab791f5ae"
},
{
"name": "biohazard",
"unicode": "2623",
- "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235"
+ "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
},
{
"name": "biohazard_sign",
"unicode": "2623",
- "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235"
+ "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
},
{
"name": "bird",
"unicode": "1F426",
- "digest": "3f219e5aa18e2f1febfd368ec133786cd2eab357db79984cb8ba07fed0eec7cd"
+ "digest": "f916eaf8f271b3767ade9eabb69594c0479f45472d471cabaf59f6e965c161e0"
},
{
"name": "birthday",
"unicode": "1F382",
- "digest": "9eb1adb0170ab851042cb3da8b64f02f4e4b63e7a07db405b55b50f5bbd3cacf"
+ "digest": "89e7c4c598ebee8ec8ab11ebe4ccc6defb7c4d2987ee2379a19b3b59827dd98a"
},
{
"name": "black_circle",
@@ -762,82 +762,82 @@
{
"name": "black_joker",
"unicode": "1F0CF",
- "digest": "1eb85b8e2b93dec221a97a1c309dee3683408f6166e1a1a1bd83cf2f64f007dd"
+ "digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d"
},
{
"name": "black_large_square",
"unicode": "2B1B",
- "digest": "0ff2112227c38ed8c30b0bddf2300e87d2a244cd7fe81886a1cb1a287a7e8bb6"
+ "digest": "cbd90dcbc2f674eafa53820548b5263c18c9845ab39937f085e85aca0aebb479"
},
{
"name": "black_medium_small_square",
"unicode": "25FE",
- "digest": "f1010aa694084ad4655a9d4ce5a1711eaab21029e31bf8798253f0ad644e8abb"
+ "digest": "ab38363c2e862b8f67c719397a09a18e1ef996eec190691fdf769f5cfb209660"
},
{
"name": "black_medium_square",
"unicode": "25FC",
- "digest": "06bf48ffbc84e71bbb90aa0f6c3f9f53533c6fd063ff168cefdb0a050dcf8302"
+ "digest": "c9ffa87c37e8ee65fadcf755176949901aec7367e02abb85e63cad60cd922116"
},
{
"name": "black_nib",
"unicode": "2712",
- "digest": "c1361df4a5ae9f2ed121d26928021e96c6865331861e1960700d39cb1bd49355"
+ "digest": "58fb23b1155102970eaa23765e7d529a21e8e545e076ec1158bf11b4de5f51a8"
},
{
"name": "black_small_square",
"unicode": "25AA",
- "digest": "d430ec419869fa1b5ba980ddeecb4c5ad5050a2b3421e45048cc184a6fc46899"
+ "digest": "f69be6de578fffce5a3e60eda690104b2ef6a855c630040104fb760a02ff1aef"
},
{
"name": "black_square_button",
"unicode": "1F532",
- "digest": "85b6587b6b2c3544ddb7bc07207b0740e437744ba134835836153899ae396135"
+ "digest": "9d818fcd08ed38cd0bbbcfd83e665aa29b3761c0d8b9806d8954d36785e267a8"
},
{
"name": "blossom",
"unicode": "1F33C",
- "digest": "029bbe385e07e2017dd918d685e107678c9c0e919a3bd1521b7a0d7c9172da05"
+ "digest": "e8cf369d4e4cdb4eccc2ebcbb35439b0344221115701daae642e58dff8544922"
},
{
"name": "blowfish",
"unicode": "1F421",
- "digest": "b5ee9f6ffabb74e3024067f016d17a631ee98536cb9c7269d55fa867f95a54fb"
+ "digest": "e706849ed00f08a82312381c76f6f9ba6cc261fbf87a839c85e7dd54138f9dc3"
},
{
"name": "blue_book",
"unicode": "1F4D8",
- "digest": "6fbf227fb9facc1957bb9dfb31749cbfe66c3afe8081347f2471fd64ef2e6b3a"
+ "digest": "4c845748fe890516b32981b0b62bf3e8e9d906840c2060179f4f844100780615"
},
{
"name": "blue_car",
"unicode": "1F699",
- "digest": "e61ef2299d11fc01e9d6c496d188a7211633946706f6e771c412368346ca16f4"
+ "digest": "eca91934eb5481726cfd897b1ed5eac306e14d02499fbe49316aaec6c72b6707"
},
{
"name": "blue_heart",
"unicode": "1F499",
- "digest": "1af8d04173e0a984360786f6031220000dd548b8c912a68fd51f2ba490a9e16a"
+ "digest": "2caa0c8d18538cc871c6fe328a52f71e1df8aabf4d1cc2f5324b261d1b8cb99a"
},
{
"name": "blush",
"unicode": "1F60A",
- "digest": "d615cda0f7c185ed8a92008204043ef769f3b7fb5424d595aeaaf3827bcdbd73"
+ "digest": "3bfe8d603cfa39999c164779f666d39bbc507f124ba80233ee72da7b3b0c0457"
},
{
"name": "boar",
"unicode": "1F417",
- "digest": "c23a06db0337597e361ae581eacd4faf9926c6b7db0510d3599eb2e2a73315cb"
+ "digest": "c9d67479cace427ac3c30460fcffa1bf9a8e5262c0390962405dbbe6bf830fa6"
},
{
"name": "bomb",
"unicode": "1F4A3",
- "digest": "0099e7435eba35f4f3ad273993293693a8b5cd110567c95ed83e5b4e2d0978ff"
+ "digest": "0155559abc4084f80e9b0b2a2091b8710ddd6369993b7fdd0685f4f8c2fd7e6c"
},
{
"name": "book",
"unicode": "1F4D6",
- "digest": "152408f2ff9949b7cbe57f623e4f875aa8dd0b02317e03cc914e1ea3712b3fc7"
+ "digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf"
},
{
"name": "book2",
@@ -847,32 +847,32 @@
{
"name": "bookmark",
"unicode": "1F516",
- "digest": "a2e0c6f5466c1b2fc148b20f6afcf4a878f4df55b0181f61fffa3ff727dcb251"
+ "digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d"
},
{
"name": "bookmark_tabs",
"unicode": "1F4D1",
- "digest": "16135d62ff440722bd1ce8f84219be6a5eb3120a1597bfda4aeed4a2d9e7d7b2"
+ "digest": "c8fc7c9f3f82e1ccc97fc591345fdd88b09eec0fca428d8d4632a121cf1bc39a"
},
{
"name": "books",
"unicode": "1F4DA",
- "digest": "ba019e4174639440caec424b30dfa016fe71a6f7436fe63025a2e3609ebfc012"
+ "digest": "cbcf55d39dd05d26ef7350bc51e0e2f064f78bb8f59d407b516d63f68558f8e4"
},
{
"name": "boom",
"unicode": "1F4A5",
- "digest": "ec26246935c99749950612d69c06435ccdc126f14426a48a7599c5b6b91d9d58"
+ "digest": "f5400e9583f7f997cd2385f21379f6229424a9b221445bc8f36c0bb64bdb3168"
},
{
"name": "boot",
"unicode": "1F462",
- "digest": "7ed639d52e285b0f46064dd4e1f4a8fb5814e1b2dc47c6f93cb349a6ac7ea97a"
+ "digest": "b4706ff35909a6fb759a3b8a797e90cb67ffc60e4853386a7d89ace9693a9364"
},
{
"name": "bouquet",
"unicode": "1F490",
- "digest": "b699f13af218560344f3571436f87b6f8c5c9f0fa0308836937667241b3fc7aa"
+ "digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f"
},
{
"name": "bouquet2",
@@ -887,77 +887,77 @@
{
"name": "bow",
"unicode": "1F647",
- "digest": "5e260c38cfc80cd2f20ef78d982126dbf90934f7afa12c96d0b7b413beb6d4e0"
+ "digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd"
},
{
"name": "bow_and_arrow",
"unicode": "1F3F9",
- "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3"
+ "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
},
{
"name": "archery",
"unicode": "1F3F9",
- "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3"
+ "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
},
{
"name": "bow_tone1",
"unicode": "1F647-1F3FB",
- "digest": "d3ec7ef70b355ba310d6fae7130a4e4cd11526b6e219474b5678a2b3ba1077f0"
+ "digest": "995c8400ad60d5adc66c9ae5e3c0ecf56c48b478ad79418d45b6289933d25bdd"
},
{
"name": "bow_tone2",
"unicode": "1F647-1F3FC",
- "digest": "c2905c0feba15fbc533cc6b36038eeda30f729182aa544f1d9164f5ccfed64d5"
+ "digest": "af89eec2fccda99d9bdd373b2345595882fee1c0a15d29af9028089e20255325"
},
{
"name": "bow_tone3",
"unicode": "1F647-1F3FD",
- "digest": "298fc646d96c307eaa137c80b403d8355539ed8af13d3954a4ccacef67d341fa"
+ "digest": "015d8122abdf2d0caa03815545f50fb7a71e05dacd46aaa133cc9ace5192f266"
},
{
"name": "bow_tone4",
"unicode": "1F647-1F3FE",
- "digest": "27db8401aa62a2544b24ff839b332958b5e8c3ab3fd7a289d3c62c654705da60"
+ "digest": "e8409096a795b775def654d36aeccb8eb91e83d7d1b32145cd73fd0b7b9e885c"
},
{
"name": "bow_tone5",
"unicode": "1F647-1F3FF",
- "digest": "168cdf834edb54723cf1c32311d4117c288132c5f76d6c415726c7484158c52a"
+ "digest": "d87042cde8dbad9fb1a91a2ec60116e27b4a76388b5779d771a0bbae12a2814d"
},
{
"name": "bowling",
"unicode": "1F3B3",
- "digest": "0e888bcd1a5cc1ea7b07cea255ccb04dcdc87b0337b74cdc96a708aad7975768"
+ "digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662"
},
{
"name": "boy",
"unicode": "1F466",
- "digest": "f349ab3e1015b4ccda5faab6a355f9c38e36e7c1cd667084563a14a2b11036ea"
+ "digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1"
},
{
"name": "boy_tone1",
"unicode": "1F466-1F3FB",
- "digest": "4d04a5e45c9f9749de580321a212e14304b4ffcd229fa971fb59d97e6124262f"
+ "digest": "c0e2f0483715b239fe145b0056566f7a3a722319d9a87c1e66733dff1916a19f"
},
{
"name": "boy_tone2",
"unicode": "1F466-1F3FC",
- "digest": "0c9d6b6b1b3da68b9ef1f0f01efa4d170a48cfc66de4f577f8669c160b81cc97"
+ "digest": "0001d0bd1ff4dbd898604ba965b4039d09667d955bc0349301b992f9ab6dd7fd"
},
{
"name": "boy_tone3",
"unicode": "1F466-1F3FD",
- "digest": "7dbecace78edb2aceffce6cb4d49ca132b93d80c26a8f1526a18832a2f23454a"
+ "digest": "e0f08755955fd2e0bd1c5d5e84429b2a234b24a744bb50bb9f1148495b2b29f9"
},
{
"name": "boy_tone4",
"unicode": "1F466-1F3FE",
- "digest": "49f9c633afa8ff81068c78717e0012f8936fb3dcdb8b57342410f57f0635ae7c"
+ "digest": "04b6bfee58a26b1ce2e5b403504a7033aaf395f03f5cd23e824f32c90c395fe6"
},
{
"name": "boy_tone5",
"unicode": "1F466-1F3FF",
- "digest": "17e2ec379c7b542e6c2c5deef992af5f1fbaa3e288d1f71c8c984fb91a698cd4"
+ "digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5"
},
{
"name": "boys_symbol",
@@ -967,72 +967,72 @@
{
"name": "bread",
"unicode": "1F35E",
- "digest": "43697495538bfed11ed75213af8b1bdc14ef359d9b472cd7f9130fcb0a198680"
+ "digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86"
},
{
"name": "bride_with_veil",
"unicode": "1F470",
- "digest": "37e75fbb2b0d06c900d51269b99107c60b61453dbf218b54df3011a455cd6dc3"
+ "digest": "8e24bd91c3f564cf6148f2b3b4a7d692c11dd059e76a13331fdfb04ae060ea70"
},
{
"name": "bride_with_veil_tone1",
"unicode": "1F470-1F3FB",
- "digest": "44072e54e0618d2675a5bfd6572108590e51e8e733381e091e8754ee96c2cf20"
+ "digest": "0bd2f16f72586f50e768b14b9b353f2e98ccbb2581a568c33b06be56e70ca063"
},
{
"name": "bride_with_veil_tone2",
"unicode": "1F470-1F3FC",
- "digest": "f0acd961e108db9d9dd5d1b06e708b2eb6a7ef7235d6c8678b9319077faf4fa8"
+ "digest": "e5463f811b2075754f0718b891757cd2e81071edf7af2215581227e1aad1d068"
},
{
"name": "bride_with_veil_tone3",
"unicode": "1F470-1F3FD",
- "digest": "3f7adddb41ead3cd07098799ab2a5b8e8842344307d9045264403fb685f20555"
+ "digest": "e5a053a26f7ccebae7eb12f638be5ed80f77b744708d783eab2eb8aa091cf516"
},
{
"name": "bride_with_veil_tone4",
"unicode": "1F470-1F3FE",
- "digest": "5f7199fd99319651f3a7b3553cc5387c59b65cac1eb020441e19b5c12c807dc7"
+ "digest": "410e23825e4401460946dc67a618bd3ace6e1a7c07dd88580a2349423685261f"
},
{
"name": "bride_with_veil_tone5",
"unicode": "1F470-1F3FF",
- "digest": "4b1f6c33dd72a3a11c764bb00e7be7441b39c7af78aae52141276a279d63ab78"
+ "digest": "454e87e5a74e13e5b4993541231516fbbe6dbe9f990e1a6f3f4a744d7d4c1615"
},
{
"name": "bridge_at_night",
"unicode": "1F309",
- "digest": "f81cc36de8edbdf3fe4d55932d5c6c8ad429487ec1f7af044611b6dc950ee09c"
+ "digest": "9d3cda5a59e27e3c90939f1ddbe7e998b3ea4fcacfa1467dea0edf39613c2d7f"
},
{
"name": "briefcase",
"unicode": "1F4BC",
- "digest": "a3c3e802191f3e131683dac1fcd81e294dea72af8e65c94972990924c79c5619"
+ "digest": "9d00d6a92632aaadc71b017f448c883b27eb31a7554ebb51f7e3a9841f0f7f2b"
},
{
"name": "broken_heart",
"unicode": "1F494",
- "digest": "4dee349274c2ea44d1c0395cbd39356b88897b0c45040aa40d8cb2607ee67420"
+ "digest": "c7ca53f444d72e596af46b61ffbc9e7c18a645020c22691e44f967db98dbf853"
},
{
"name": "bug",
"unicode": "1F41B",
- "digest": "bac4660ee8dcbef0023691804ee3fad3ea3d4bac20d847a5913cee6e7dca826c"
+ "digest": "0dccb1d5eb91769377b4c5b310f007b60f54a5c48ba9e467b3a06898a4831b90"
},
{
"name": "bulb",
"unicode": "1F4A1",
- "digest": "af5394230f95781c7eb8054b1a13732a6e6170318599c79e9ca2a816a5b821a2"
+ "digest": "ccdaa2dfde5a88a347035a94b9d4d86cfc335ce0a73292423f5788a4bd21a5a8"
},
{
"name": "bullettrain_front",
"unicode": "1F685",
- "digest": "59afcd289500bd4148b1b91f560a5ce8ac9e1b52eddb8fec857ff5d171f017fb"
+ "digest": "5195a6a6d23f28e1aa5ebac6ede0f6c6a8b7ff33a9edf034814f227fe976177a"
},
{
"name": "bullettrain_side",
"unicode": "1F684",
- "digest": "79ff8f579081a2f1c3b05311a18ca432adb026a7860875cea4a5460e49b2a474"
+ "digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7"
},
{
"name": "bullhorn",
@@ -1052,37 +1052,37 @@
{
"name": "burrito",
"unicode": "1F32F",
- "digest": "4babb1af1136ab2334d26495b0be779d0bcc9516fd956fc07ffde427d11122f0"
+ "digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf"
},
{
"name": "bus",
"unicode": "1F68C",
- "digest": "476e7a5e92f64038e5012205395efead51f1c10b3edb25380f38da97e2412edd"
+ "digest": "192850b762edad21ac8770df38b9cae6d2bc1697a838462f3e36066bfb4eee50"
},
{
"name": "busstop",
"unicode": "1F68F",
- "digest": "3bcf82872ab6abb0278238c71bd004a40c46696bdda05f54c153d45d6fe88f15"
+ "digest": "adabb1ec36402b33feb636eae3656e5a8b51ff1071bcb14125d8ab80d6d12d2a"
},
{
"name": "bust_in_silhouette",
"unicode": "1F464",
- "digest": "2230844993ab011fe2756a1aa3873ff7d5f7d888bddec408ba0b32e4f6003570"
+ "digest": "277ae43301f1e49e0be03c8e52f0dc7b70c67f9d146bca0a14172e0098f115e6"
},
{
"name": "busts_in_silhouette",
"unicode": "1F465",
- "digest": "d1c3cb6d437616834425a53621c0bc0a6b368d745dd9da2300a3db4543d57660"
+ "digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b"
},
{
"name": "cactus",
"unicode": "1F335",
- "digest": "e87588e6548d201db903dc0523b3ccc83c6b559981d743eae1504ce668cd8be4"
+ "digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd"
},
{
"name": "cake",
"unicode": "1F370",
- "digest": "3947783d128018f5e396602d0492cb5c31e8e8df98af01eda7cade71aea8d989"
+ "digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b"
},
{
"name": "calculator",
@@ -1097,42 +1097,42 @@
{
"name": "calendar",
"unicode": "1F4C6",
- "digest": "00bb700dd88efbc43bc64263491cdf77965130b1dc23f31e682905c3dfe4040c"
+ "digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3"
},
{
"name": "calendar_spiral",
"unicode": "1F5D3",
- "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a"
+ "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
},
{
"name": "spiral_calendar_pad",
"unicode": "1F5D3",
- "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a"
+ "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
},
{
"name": "calling",
"unicode": "1F4F2",
- "digest": "2375828085f2efd17b8a5ebb3cfec1e420190913328a7a0dd9ff0f67c7249ffb"
+ "digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91"
},
{
"name": "camel",
"unicode": "1F42B",
- "digest": "9ff789ab50b51cd9e7fdc7fbe8d6f913fda95dfd425949f97974548652a53ce1"
+ "digest": "5f927927a7ab1277d0dc8b8211436957968b1e11365a8bf535e9bb94f92c5631"
},
{
"name": "camera",
"unicode": "1F4F7",
- "digest": "d95192b9ba0f566d8874099125def031e15297d1306989ea9b6a49f7b9b56661"
+ "digest": "fde03e396822a36cd6ae756ede885b945a074395264162731ca5db47a3b39d80"
},
{
"name": "camera_with_flash",
"unicode": "1F4F8",
- "digest": "4db6fb3fdb9a004537dff97f4197c7ed87c9c978ba9ac562ed8bb7c1fa260d38"
+ "digest": "9afd380208187780f00244c45d4db6c5ea1ea088d4a1bd8fc92a8f3877149750"
},
{
"name": "camping",
"unicode": "1F3D5",
- "digest": "f0855dc78bf6f3d06b3c2fc19180c8ff23d9e22871658fcc26a8fde08d328a0a"
+ "digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9"
},
{
"name": "cancellation_x",
@@ -1142,47 +1142,47 @@
{
"name": "cancer",
"unicode": "264B",
- "digest": "b990f85e9f62017d99526244eaef5c5e56f8808698011e85d44de1d2ed87f1a2"
+ "digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6"
},
{
"name": "candle",
"unicode": "1F56F",
- "digest": "5eefd555951e65298583009a307acc6fb6d02c88325ef3adf231717e75e5a333"
+ "digest": "211c04dc3a91b071c284d4180ed09f9d3320e3fd6ba8a9fddd0677bc97fd12cb"
},
{
"name": "candy",
"unicode": "1F36C",
- "digest": "f14203c408173fbb94b4ee69d6de67226a17dc51b0cbd776f62623ee03fd2eb3"
+ "digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100"
},
{
"name": "capital_abcd",
"unicode": "1F520",
- "digest": "2a7cc876218b8c244b9802448ee25ce5004671a4f00ea950a636d8c3b766dbef"
+ "digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa"
},
{
"name": "capricorn",
"unicode": "2651",
- "digest": "03a5fd064c10f47c7fd0ae318c573bb559c269b1b2d61b45aa5b8ce9b5fbd9df"
+ "digest": "f11abad102603737b55486fe2ea4d01f28b203394bcd84f19a7948156e6c4b96"
},
{
"name": "card_box",
"unicode": "1F5C3",
- "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a"
+ "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
},
{
"name": "card_file_box",
"unicode": "1F5C3",
- "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a"
+ "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
},
{
"name": "card_index",
"unicode": "1F4C7",
- "digest": "150950903eccb468981c58b87ed7c1ba44e17f52627d695f660ce96b3d9d6e8e"
+ "digest": "86e187e0a72ca5d00207d6ef34d66ce15046848a831c2b5184fb840c5332a2a8"
},
{
"name": "carousel_horse",
"unicode": "1F3A0",
- "digest": "d6862085550fa139a147dceb1b2b9f950a08dcd01cecd8b8697f9c7992ca054e"
+ "digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe"
},
{
"name": "cartridge",
@@ -1197,17 +1197,17 @@
{
"name": "cat",
"unicode": "1F431",
- "digest": "002208c0c9165971853ee05cd05513175a913376a462a345a939d73401c6acb7"
+ "digest": "e52d0d3a205a0ba99094717e171a7f572b713a0e21b276ffa4a826596fe5cafc"
},
{
"name": "cat2",
"unicode": "1F408",
- "digest": "fbdb726cc035f83784dcfe2d9adb85f8aeec429064aed5c5ca0b8be406068aa5"
+ "digest": "46aa67a99f782935932c77b8de93287142297abe52928c173191cf55bb8f4339"
},
{
"name": "cd",
"unicode": "1F4BF",
- "digest": "bd4d4eef2cc0b1e4ee1f5280f922743e76f27d35836987801b2b48969eac17d8"
+ "digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b"
},
{
"name": "celtic_cross",
@@ -1217,302 +1217,302 @@
{
"name": "chains",
"unicode": "26D3",
- "digest": "a6a915d9c361e1564e13cf2d33ad5df3d684aa349b8dc5909e6343d67401beb9"
+ "digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2"
},
{
"name": "champagne",
"unicode": "1F37E",
- "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713"
+ "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
},
{
"name": "bottle_with_popping_cork",
"unicode": "1F37E",
- "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713"
+ "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
},
{
"name": "chart",
"unicode": "1F4B9",
- "digest": "9fd5f8cd99988bbe0fabc89a0b23e28d1468641d2f9468e82b7148a1948d8236"
+ "digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f"
},
{
"name": "chart_with_downwards_trend",
"unicode": "1F4C9",
- "digest": "6fe456d76c0a996c12049057b5d60129098a9deddfa2d133cff5c4400e4595a0"
+ "digest": "5db7ccbc37665736a9c0b2f50247dcc09e404ec37f39db45b7b8b9464172a18c"
},
{
"name": "chart_with_upwards_trend",
"unicode": "1F4C8",
- "digest": "e83cc4cf4228bd77e030a19755b11cf75cf671f40973c23e240afa54d9de478e"
+ "digest": "bc4ea250b102fe5c09847e471478aff065ad3df755d9717896d38d887d9c6733"
},
{
"name": "checkered_flag",
"unicode": "1F3C1",
- "digest": "77501c2c66af31f72f5c05f21e87598cd59740b5cfc02926c66dc755bab3c3cf"
+ "digest": "0e77180e0cf9fc87e755a5a42cf23aec6bf30931db41331311e97ba0be178b78"
},
{
"name": "cheese",
"unicode": "1F9C0",
- "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1"
+ "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
},
{
"name": "cheese_wedge",
"unicode": "1F9C0",
- "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1"
+ "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
},
{
"name": "cherries",
"unicode": "1F352",
- "digest": "5a0ba73039e4b56e3d16a1c70ad992f41af7a16f6d5ba4b5337bdf338276f0ff"
+ "digest": "13b8db9e7e6eec8509aa80c762966e1bf3538fcb1ac3d6eab18ee4da1528cf84"
},
{
"name": "cherry_blossom",
"unicode": "1F338",
- "digest": "b40533225291f539ffe97e4ab1d70d07e179b2f9345b2814355164d0407cf3bf"
+ "digest": "af3083f5f8dd94936113f2e16caba5aec7a774d5589aa08bf5de82a2d278cc66"
},
{
"name": "chestnut",
"unicode": "1F330",
- "digest": "6a2a37899d28326daf36965b343b2646492c2c0cee8871321cc17315d6252a9a"
+ "digest": "9f85b79b207a69ab81ab88dcef04954000965b039b4cf57de5f1b381745ab98b"
},
{
"name": "chicken",
"unicode": "1F414",
- "digest": "13d770684a11ea10c0ae7570a98c5dfafd4bfb78ac3f72f46729aef9060b85c0"
+ "digest": "57ceb4459d183740009caac6ebed089d2f1e12f67c138e1be1d0f992313c0ac4"
},
{
"name": "children_crossing",
"unicode": "1F6B8",
- "digest": "654d2502c1edc57c5ab4237df76db3121f6b8735eb13d30bffd305605a083445"
+ "digest": "0ded7d9aca0161e8ef8e2858c3c198e70e4badc7105ac3a6886e06975de19106"
},
{
"name": "chipmunk",
"unicode": "1F43F",
- "digest": "1ae3c838450afcbbe8a96992481dde252e343ab83546d0789ebed81a78ca9188"
+ "digest": "5b0dc1a859163097727ba2ba5ffca38b0a54d925eebb089977d28d0b4d917a3f"
},
{
"name": "chocolate_bar",
"unicode": "1F36B",
- "digest": "2486b7265048eb2294d6be0a0a8a4d6067df95721ace9d131d8f715a27ba8cf0"
+ "digest": "dd273e5050488acaf885f8a18b6e2b3901f69c5b39fa6465fb60621783d4109a"
},
{
"name": "christmas_tree",
"unicode": "1F384",
- "digest": "454c08870eaa84283c19731ed3b10c4868d2e2f0cc44f2feba0de9ba4cc9c4e1"
+ "digest": "ce60cbe2ebbe8057be8edea2392455fedd2bcda64a0a831f6a1942028af7e747"
},
{
"name": "church",
"unicode": "26EA",
- "digest": "b62e838ffb0dfefeced1707359437b6815e0721783b549212282e08617402f6f"
+ "digest": "2c328456528f7336e59443e20ec3ab22fe71f1fccb1dd50d0ad68eb206937557"
},
{
"name": "cinema",
"unicode": "1F3A6",
- "digest": "6df56f6a0008d0352740d1e045ffdb702e80c2a6d88b6db1a8bcd27eb3c12dcc"
+ "digest": "4c26dcdc76f93dbc2a1dc49ed4e132b8e8f2b7cdc1acf5e09b3dfd99430d97cd"
},
{
"name": "circus_tent",
"unicode": "1F3AA",
- "digest": "f8b7a7f4cf4f9efd20423acc30abb3a28e2a5183b3e39f5cc88e7e0ed7757d64"
+ "digest": "fec5f2a06222be8be549178b29720343cc00145177ec387ca4e6f3432481fe77"
},
{
"name": "city_dusk",
"unicode": "1F306",
- "digest": "8779066dc9386d05c951b1df1753983c2937a5f3b84d5fc09ed0b172d4ef914e"
+ "digest": "bba345e949dcc51f5f018220f000223797970c82ead2ab9c822f9dc0847aa155"
},
{
"name": "city_sunset",
"unicode": "1F307",
- "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b"
+ "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
},
{
"name": "city_sunrise",
"unicode": "1F307",
- "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b"
+ "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
},
{
"name": "cityscape",
"unicode": "1F3D9",
- "digest": "15251a708d50fc721bd67d8abb2a517c0bade196df3b736e21d79191d749241f"
+ "digest": "ee360be7514c4bfb0d539dd28f3b2031ebcef04e850723ec0685fb54bd8e6d5f"
},
{
"name": "cl",
"unicode": "1F191",
- "digest": "104591d8e7b980cf38dcf8326d36c845384b7a4e6d94c49f36e9946484712a95"
+ "digest": "fcec2855dbad9fda11d6e2802bc0dcaabab0b5be233508f5e439f156f07602c1"
},
{
"name": "clap",
"unicode": "1F44F",
- "digest": "ed6ef8bb78ca1fa295b87222c440c6d5ba4f154f2752bf0d428941260d66aaac"
+ "digest": "a1860ce7812a9f6fb55e45761e1b79a2f8f0620eb04f80748a38420889d58a2a"
},
{
"name": "clap_tone1",
"unicode": "1F44F-1F3FB",
- "digest": "57a1fd1fa2578c30b8a47abb84e81af5f5bbc6c301a5daf0c53d4d07b017e777"
+ "digest": "18a7022e08223fb2109af5a9b9a5b4f47dc870ce4453f4987d2d0b729ef54586"
},
{
"name": "clap_tone2",
"unicode": "1F44F-1F3FC",
- "digest": "2ad4dcd513e55486f21151bf3792e1febf116574d238545b07b4290901430fdd"
+ "digest": "5954c8658b15e755d2018d8674df84d38e22ffededc4d726c6a33b709f71426a"
},
{
"name": "clap_tone3",
"unicode": "1F44F-1F3FD",
- "digest": "2d8c705d4fcc162fb65cd51e2c6683f1129ebc72fba13343533f64ede1c62687"
+ "digest": "22639b6bd3c53784a2f855d6db7bdf31621519f19dfc29a6bc310eee6421f742"
},
{
"name": "clap_tone4",
"unicode": "1F44F-1F3FE",
- "digest": "40ffd41b2b4f59d0040e9d20497e57c4e47f18aeae43fcae02be5c2f50069102"
+ "digest": "e55248dc163d1bbd118b50cd8767750ead86d082151febbc0a75b32d63abceec"
},
{
"name": "clap_tone5",
"unicode": "1F44F-1F3FF",
- "digest": "be55df1ac7600ba086c2ef6ea223ebc62271fa47876c53ade1a1c0151fdc994c"
+ "digest": "76046b8157dabbe048a07fc318122456020c9c980fc1b8ab76802330e07b3b53"
},
{
"name": "clapper",
"unicode": "1F3AC",
- "digest": "a8748398f56fd2c1e6e87fe0c77edec444df7c7dd462d43dbcea6d8de97c81c5"
+ "digest": "8149752a0e3e8abede2d433d1afab6d217877d0c76adb1e2845a0142c0cdcbaa"
},
{
"name": "classical_building",
"unicode": "1F3DB",
- "digest": "6a607b0666141b51d6e944b04f3f6188a5c026396e6105f1d2a5e6b6350cd66b"
+ "digest": "9ee0d00c43d6e22b6a3ddea67619737270cc7e9294797a19c7c60d5f92aa44fa"
},
{
"name": "clipboard",
"unicode": "1F4CB",
- "digest": "4ca1a0b864a962b111d6bdb65373b779f3fff571ffd32d029666f9b708e1ab73"
+ "digest": "bdd7f7d973c714e59d2903d401a876e6018794c7987c9ca57108c137c5edc25f"
},
{
"name": "clock",
"unicode": "1F570",
- "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed"
+ "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
},
{
"name": "mantlepiece_clock",
"unicode": "1F570",
- "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed"
+ "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
},
{
"name": "clock1",
"unicode": "1F550",
- "digest": "c0550fa0c385920cbdb775bdaaa5e812097a484c4a32e35ebbafe3a364a4a438"
+ "digest": "1778eec07ce061c9393e5abee5ca83b24e1ce61d8a75fa2e39efcb31aa160395"
},
{
"name": "clock10",
"unicode": "1F559",
- "digest": "25651ac5520505f326457364428de3679cc22ca57278d4c54cc4b60420fa7b74"
+ "digest": "601fc12ea5280a54c2e69dbb685f454e4165fe771756ed6f89016e29e683a24f"
},
{
"name": "clock1030",
"unicode": "1F565",
- "digest": "dbf682bac968fc5a3959af2b96eaaa5ee78306f6341c43c1345b94bc561a3d04"
+ "digest": "4fd155f08f797542d52cff4b0aa3ca9f080f37a41c301b82f90ff6d4693c890e"
},
{
"name": "clock11",
"unicode": "1F55A",
- "digest": "333732dd6c3184f257964bcf5a20a6111f9adb04560b5d12dc613636e846df5b"
+ "digest": "5c79dc812e812e8a01993ea633b323d654ce3a7ea258692781a4896e4ad2017e"
},
{
"name": "clock1130",
"unicode": "1F566",
- "digest": "005999cb37998adea1645d7df63b2705a42db3b4f1a734891d79af3e833764ff"
+ "digest": "41497ee2020ee5ac9aa5f9b07560f7afca7c422b04214449cfc5cea9f020f52e"
},
{
"name": "clock12",
"unicode": "1F55B",
- "digest": "6690e591bec1751e1c5472e0bf52f66779b2113e5b8c6c578e65dbb83d091b16"
+ "digest": "046bb7ffa5f5d27c2e3411ba543484d9dabb8ebf6d6e7a7e9bfb088c1813500c"
},
{
"name": "clock1230",
"unicode": "1F567",
- "digest": "549f3921bcff7f330c5a41e6756d8c15601f1f8278b35b369148771c60be2a6f"
+ "digest": "bbfe9db5a2043aaba19a7a2a0185c7efcebf1e8c9263b8233f75b53c4825f0f4"
},
{
"name": "clock130",
"unicode": "1F55C",
- "digest": "9332ef07a9dde8ccaa1e58a3e97edee0601a1152fc6d351b782816c838d2a408"
+ "digest": "8662cb395ee680c2781123305c4c8ce8c0df9565c2c942668940be540cc0c094"
},
{
"name": "clock2",
"unicode": "1F551",
- "digest": "9d1ec8fbdae627880e1c067c10d6a40f1e4494a246c77224b3cd7b287554c4b4"
+ "digest": "42f7429748b612dce7de77221cbbc710655811f7bb23e2a986c36e6d662f0ec4"
},
{
"name": "clock230",
"unicode": "1F55D",
- "digest": "3578a39c28695d4e617a648a1eb44e0bb5a8a11dcbe04fa2eb2aea0a60589067"
+ "digest": "e710b6ef14227cd240ea3e2a867c8ef45b5c060adf3cb30ba9077c2351fe6677"
},
{
"name": "clock3",
"unicode": "1F552",
- "digest": "c2e2a27301b6ac27dc359be590448eb1e65fe87211f1af30a473d8bde4f3db47"
+ "digest": "7340d465b398a378211dff9ec806db579d061206fd6fc238623d070cfe0a55ce"
},
{
"name": "clock330",
"unicode": "1F55E",
- "digest": "7a77cf8cf9a98f4767a2dca1d3795be45938eee185db81120d85cedebe128899"
+ "digest": "7aa4a15cc8de04ed3bdeb0f8a54a7915065f2809a07054e002d89926c9766831"
},
{
"name": "clock4",
"unicode": "1F553",
- "digest": "0945c4199400d546350cfff25bc9e9160789d1cf9890b3318bdc462ac6cc9782"
+ "digest": "36fd88e81ad488b0ec49a911a838693281573fa14736ae4a6dd1c40a4ff69bb1"
},
{
"name": "clock430",
"unicode": "1F55F",
- "digest": "9fdb6f1fa076c4c6a395dbf6db27499ee447b3558f3aa64d913686c360e428a8"
+ "digest": "7bd5dd71e89d95dcf18b9e8c1fe2a353a7da3b69aadb8dda80ee9bafb05da58d"
},
{
"name": "clock5",
"unicode": "1F554",
- "digest": "855b3500eb6d20bb6e51d3a6c9d1a5131c06404c6c149841c7cca52201036428"
+ "digest": "aa406409e56a0bfd8c850e44efe45fd190ffd7bf7061e934ed7928dfbdfc9eba"
},
{
"name": "clock530",
"unicode": "1F560",
- "digest": "a6ebd9f884d45a1f43650351a1f1da9724bc044d7da2f6d99ffb3d1fa0c31c5d"
+ "digest": "25dd3bcc53ddd98eeea498d7dbd4c306ef39dd033f15909063388a0800febf41"
},
{
"name": "clock6",
"unicode": "1F555",
- "digest": "e38f9fc4f87f12ee602dcf2285d59dbc343fc0fc37662992cfe9866c20f58e87"
+ "digest": "0a321eaf1bc5db8436bbadac66c45ba257fc98ad4c7569ce3fc6602c824b6d7c"
},
{
"name": "clock630",
"unicode": "1F561",
- "digest": "735954a650791fc38c845c43998023e652d36e55534850e43952878b8804b2f1"
+ "digest": "55a4c5a665fdd38a724e9357a93c55401fcd5f1b13078c25754bd70c3fc4ccec"
},
{
"name": "clock7",
"unicode": "1F556",
- "digest": "2c4244ec4019e9624e6ea5a751bb735ab87bead33b1ea160265c81bba3c2f736"
+ "digest": "6154306545716e865da0ec537ee4f22bfe6c7294502a64a2dcf425c587d0e2a2"
},
{
"name": "clock730",
"unicode": "1F562",
- "digest": "0bcf20e30be1bb23394696770301867e307f8e5014e0ed7d75ed96efe34d625d"
+ "digest": "6925654de642e50f84661f94364a96c87757d73fffe766aacbf4bbd70130547b"
},
{
"name": "clock8",
"unicode": "1F557",
- "digest": "af454047a1765ef1c8355969302a826d4c47f5c61a6ec47fdec3510a8003b0d8"
+ "digest": "9be2d189c7ea56d39fd259f84853d753c1cf33e64f8ed57f86f822d9ae23a1ee"
},
{
"name": "clock830",
"unicode": "1F563",
- "digest": "e48b81dac055dc6d5f7832cf34368329c573d03b35bfe076fed1c6e6d48a82e7"
+ "digest": "16878613c0000d2f558c88d080551f424a8bd9df1358e0f931dd25c3da68f2d9"
},
{
"name": "clock9",
"unicode": "1F558",
- "digest": "f2a3d1bc029dc0e6406cdaa96542e77503e4cfb79d99c69cb454b8cf635a73fc"
+ "digest": "1d1e7e3c9d085ffa5b7c0f3d9fd394b734f16ae3b60df09af50fe6c8d4f3c8bb"
},
{
"name": "clock930",
"unicode": "1F564",
- "digest": "bb1b2b83052e8e6fb97c48c13bce0d950907e044eb2dabf21d7fed321f75110b"
+ "digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74"
},
{
"name": "clockwise_arrows",
@@ -1527,102 +1527,102 @@
{
"name": "closed_book",
"unicode": "1F4D5",
- "digest": "afd6dae5fa0f59330fc2adb922e92b3410a33a80a2667651718c7dac588010bc"
+ "digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f"
},
{
"name": "closed_lock_with_key",
"unicode": "1F510",
- "digest": "d0ed5c00f939111ce86f9c741b733b22e04ebbd871aa33da3eb0f46a6f38b707"
+ "digest": "e39adfe9b30973bca16472c2b7e6462b064a93b9d452aa48edd74c727641a83d"
},
{
"name": "closed_umbrella",
"unicode": "1F302",
- "digest": "3ef08b299f9170007a5433fe82d0953bf0f75b6685d0ce58972f9af032dc471a"
+ "digest": "2cc0592c74601f7439e88c3c1ec4f05e3459608ef1ea6558c5824ed7c3889727"
},
{
"name": "cloud",
"unicode": "2601",
- "digest": "d1e7932551e85c6e86bfb3b41f0c936a6d0953bf9f9119b8cca3eaed22ac0c01"
+ "digest": "5b3a19718dfa8a381929665afdc2284464d24020c8dd0caff4dad465a1f536ba"
},
{
"name": "cloud_lightning",
"unicode": "1F329",
- "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4"
+ "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
},
{
"name": "cloud_with_lightning",
"unicode": "1F329",
- "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4"
+ "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
},
{
"name": "cloud_rain",
"unicode": "1F327",
- "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2"
+ "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
},
{
"name": "cloud_with_rain",
"unicode": "1F327",
- "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2"
+ "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
},
{
"name": "cloud_snow",
"unicode": "1F328",
- "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149"
+ "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
},
{
"name": "cloud_with_snow",
"unicode": "1F328",
- "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149"
+ "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
},
{
"name": "cloud_tornado",
"unicode": "1F32A",
- "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1"
+ "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
},
{
"name": "cloud_with_tornado",
"unicode": "1F32A",
- "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1"
+ "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
},
{
"name": "clubs",
"unicode": "2663",
- "digest": "5fd19fadd3b0887a6a59819ffbbe33a061055c043200700c31be30e14a5d36d5"
+ "digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138"
},
{
"name": "cocktail",
"unicode": "1F378",
- "digest": "cf096ebe15b4053702d490cd96f04d565b4993529bcd6d8d50cb821200d1cd92"
+ "digest": "3792def2cde885cf32167f04904d3b0b788388e8af410c63e4cd31550feba775"
},
{
"name": "coffee",
"unicode": "2615",
- "digest": "6ea6128e353d9f74aee99caaaaa30c53f996fb242bf3bffb0fa92e6b4d373e57"
+ "digest": "0d29615a7a67d3aafa257b909bb915dc74fa8f854acb0d9a2c29e94eedf80326"
},
{
"name": "coffin",
"unicode": "26B0",
- "digest": "b59772d7aa262c4d7433f9cdf76d50011f4c63421b730c8ab4a08675f730c39f"
+ "digest": "78eccc1aad2a822649fba8503d4d30354bef367c4271193c40ddb692308f9db8"
},
{
"name": "cold_sweat",
"unicode": "1F630",
- "digest": "f0d0057bf01db8d930f6e4632c5bf8d0b1bc709bcfb6463a1f1973b5f1d70a83"
+ "digest": "f53aab523ed3fa2224a16881d263fb5e039f163380f92feb2c63c20f9b14dcd2"
},
{
"name": "comet",
"unicode": "2604",
- "digest": "00252ec55d1846d95c8d4c704b35251232d9810029fc215a7da08262dd1f3541"
+ "digest": "40ce93e55c6e57a88d80670b37171190bd5ffc87b7078891d8de5b15795385c5"
},
{
"name": "compression",
"unicode": "1F5DC",
- "digest": "432fbe66e5e3c38ebfeb4eb03465667a1e1be868b4afe510ec95eadda6481bde"
+ "digest": "c8841f7afb5345f1c31da116a7fb41d07232ea58d3f7f1a75c5890aa1a80bfd6"
},
{
"name": "computer",
"unicode": "1F4BB",
- "digest": "99777be010488867c7872b2e235be7c35b1a6f28d92baa921b61ced5491c0257"
+ "digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93"
},
{
"name": "computer_old",
@@ -1637,237 +1637,237 @@
{
"name": "confetti_ball",
"unicode": "1F38A",
- "digest": "e77d0c0970d3d12e123e548639fc0fa3ce41668667e4be55baefc09dfaa22cb0"
+ "digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10"
},
{
"name": "confounded",
"unicode": "1F616",
- "digest": "0f51db64149151d3d7ae5dce08c9af3d064123524fa36fe1f51a78cbd966b6ea"
+ "digest": "e2ff3b4df65d00c1ca9ae0cb379f959ea2cecefb3d676d4f8c2c5f2c103da4f6"
},
{
"name": "confused",
"unicode": "1F615",
- "digest": "ed23587432c1be98356156784ca4fe0b374b7b3b371660d45cfb0a1efd44e322"
+ "digest": "118d7f830ec08a3ac4b798eebb77a989b8c142f2588727181be4a2548e3c4f06"
},
{
"name": "congratulations",
"unicode": "3297",
- "digest": "2a46d640bf24fd4dc7649baf4b28c4adb30eda8d24d70eda07036c85b48195e0"
+ "digest": "02fd1338c54fe5f9a0fd861f23c56edc1d39bcd3140b68f0f626f9e2494d2d1c"
},
{
"name": "construction",
"unicode": "1F6A7",
- "digest": "73fac9fb5eb91954b0f998f9d05fb953241eed988c134fa42477393159fa34fa"
+ "digest": "c3a0401331111b9eda1206bee5f322db80b0870547d307b10dcac1314e4078c8"
},
{
"name": "construction_site",
"unicode": "1F3D7",
- "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
},
{
"name": "construction_worker",
"unicode": "1F477",
- "digest": "2be436fa7ad0a31e328fc6f776044bd1eec35c99541ced891792e3bef738d0a0"
+ "digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6"
},
{
"name": "construction_worker_tone1",
"unicode": "1F477-1F3FB",
- "digest": "172cebc84f91237a85292c5ab0a105cc3abbb96e7423c4ae81feffd00bdb3b26"
+ "digest": "fcd927405fef4486105cd3aff62155467d21cebbc013924d4b52b717b566602b"
},
{
"name": "construction_worker_tone2",
"unicode": "1F477-1F3FC",
- "digest": "3e9b96ddfd639eefda99ad3a0ad26a28a0f2c8be72988c2bdbd648e6104638b6"
+ "digest": "d1ec773828936c703dd6e334e696dc3cf7c34c0a8ec691564a384b735cdeaaba"
},
{
"name": "construction_worker_tone3",
"unicode": "1F477-1F3FD",
- "digest": "11f83c565168dce5ac2387b873769d85ec4087171d6e92fc766c209ea06cd4f3"
+ "digest": "37c114d6879b9b32b800b0d4cf770dcbe04d1455698130ecd709a0cb9dea880b"
},
{
"name": "construction_worker_tone4",
"unicode": "1F477-1F3FE",
- "digest": "09e320e78e3a2940f0c5a0ef9a235ab72c51e053fd8ff433843fdb62571c8e70"
+ "digest": "5264996c1bedb6061a0dfdddce233d863bf308d27127ad152b63bfd983162cf7"
},
{
"name": "construction_worker_tone5",
"unicode": "1F477-1F3FF",
- "digest": "7ac2a1a0038e7aefea889380be604a98255823587e90799165f7db39dd03a0cc"
+ "digest": "87051aec81fd5dfd4dc44ff0411a528ee08253e9494d37efa550694e28dde6d3"
},
{
"name": "control_knobs",
"unicode": "1F39B",
- "digest": "9f10e578b410ff6aa7cc7fe806a0f1181893765303c0ca3867b652f1392a8a22"
+ "digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb"
},
{
"name": "contruction_site",
"unicode": "1F3D7",
- "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
},
{
"name": "building_construction",
"unicode": "1F3D7",
- "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
},
{
"name": "convenience_store",
"unicode": "1F3EA",
- "digest": "1ff4351e4a4503f58ed5d35074a2112c681337e35ffe55332187481685573606"
+ "digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52"
},
{
"name": "cookie",
"unicode": "1F36A",
- "digest": "5c78ce2e721b0a3767d6ce0b59c1e88fdf94a7edc94e98c4d6b7aadb5b2aeea7"
+ "digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4"
},
{
"name": "cool",
"unicode": "1F192",
- "digest": "54a96697a5070388ce8364a5ee2e0d78a53acc8b4f6755b1359fd67252cc41e8"
+ "digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1"
},
{
"name": "cop",
"unicode": "1F46E",
- "digest": "16bee252c2a133bcf57f6d7b8372a61364744a2f662acb90e2005732555135fa"
+ "digest": "78996521bbe231d03ebea355226d8a1515f47cde7b2fbeca1037e7b7e5133466"
},
{
"name": "cop_tone1",
"unicode": "1F46E-1F3FB",
- "digest": "2fc52f3ed735e327d12dadb15f9feb7b7f720fc6857b551548a2a84809053817"
+ "digest": "8a38cd107f5f4c0b821ac43f32df5dc57facaf39fbafb98483ec00fd7df41baf"
},
{
"name": "cop_tone2",
"unicode": "1F46E-1F3FC",
- "digest": "6208f3174ced4f07ba3820ba838b247d7438d69d86eb04927333e7436e56af7e"
+ "digest": "8ab8ab086f3ff82aa4bf4760c3c822846ec2696c41d21dffdac12d5afbe398b7"
},
{
"name": "cop_tone3",
"unicode": "1F46E-1F3FD",
- "digest": "2427d30bdfe127be4d8c3870472cae191eece142c784a5c2809df938f43e7c53"
+ "digest": "fce710a99fd44a7c8af3ea01b2007e46d3ff38d7a0dff1ef26d6f893ede7e6d2"
},
{
"name": "cop_tone4",
"unicode": "1F46E-1F3FE",
- "digest": "6e73f8abdf816f3cb2728b971a5a8d006a236c1d71b2ee1788ab60329f406323"
+ "digest": "3017dd73ef475379911c5e6c79bd0f9f533dbbc5057bce6a11244faa12996ba0"
},
{
"name": "cop_tone5",
"unicode": "1F46E-1F3FF",
- "digest": "4b146465cc95ade7e9ca722e31a1b06311214dae8f7f4d95c6329d56c45b451f"
+ "digest": "a3b8807b3f2a8d6ee9bcec0339355bda486e8c930f727139f5447a4b046a6307"
},
{
"name": "copyright",
"unicode": "00A9",
- "digest": "8143583821085dfc8ac21079fe220288ba3a3b6ca3014dc5dc98b18da77589c1"
+ "digest": "cc28663cdd3f8333d9bb57b511348cde4e51bda19cf0629dccb05c8fc425e079"
},
{
"name": "corn",
"unicode": "1F33D",
- "digest": "0160502226b5f9af81763545f288dbbb20632039d7509f347c751cfdb49dc5b5"
+ "digest": "a099a0b291fa758690e6ee6c762b9ade9a0e3350a707c52d968dfffbcc467de5"
},
{
"name": "couch",
"unicode": "1F6CB",
- "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676"
+ "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
},
{
"name": "couch_and_lamp",
"unicode": "1F6CB",
- "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676"
+ "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
},
{
"name": "couple",
"unicode": "1F46B",
- "digest": "97fe611a613216a1788f9bd88a9deb4714ee123a66b5fd3d0ac916fbb4da7304"
+ "digest": "c897ba76e24e2f43a4aa261c2754800a8473f43c7ce53f9909a6af2c4897732a"
},
{
"name": "couple_mm",
"unicode": "1F468-2764-1F468",
- "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea"
+ "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
},
{
"name": "couple_with_heart_mm",
"unicode": "1F468-2764-1F468",
- "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea"
+ "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
},
{
"name": "couple_with_heart",
"unicode": "1F491",
- "digest": "d9701173a5e8dff052ab6a15a42494dbb61dc7146d3734c82916abc9c05f76db"
+ "digest": "420bfa81bad10365550c77a98e1c07eb00d03663fe7b610fab1aca8a0a9d201b"
},
{
"name": "couple_ww",
"unicode": "1F469-2764-1F469",
- "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06"
+ "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
},
{
"name": "couple_with_heart_ww",
"unicode": "1F469-2764-1F469",
- "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06"
+ "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
},
{
"name": "couplekiss",
"unicode": "1F48F",
- "digest": "e722730de82397da7c8f88d79319b391e8f01fbe4a9133850cc92ad34e77bd82"
+ "digest": "1acfef9d375c4c1deb235babd856b0f90ad4f3194751694cb6abb44f00f29e42"
},
{
"name": "cow",
"unicode": "1F42E",
- "digest": "dcc1efef2f02588806a156ed43da959c587d4c576ff6badec77f820ed3ba507f"
+ "digest": "d71c854ff8b343ee24b8c2b9d56c7cb3fc6fa1a6dc0d7a137841b9f646e6d71b"
},
{
"name": "cow2",
"unicode": "1F404",
- "digest": "dcf59f92fd0a37b2ca720bcda606defa4357b58d8f4ad15c1288ad8d814b2bc7"
+ "digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339"
},
{
"name": "crab",
"unicode": "1F980",
- "digest": "59d34a4e92326ebeab188d9e33b25c20f4d54d187c274713fa3256b03b9e662a"
+ "digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5"
},
{
"name": "crayon",
"unicode": "1F58D",
- "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31"
+ "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
},
{
"name": "lower_left_crayon",
"unicode": "1F58D",
- "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31"
+ "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
},
{
"name": "credit_card",
"unicode": "1F4B3",
- "digest": "708c0e7008e06e5d1b3b4e68a7e0ada9f4ae22ab6c28285d81a340f913fd9a84"
+ "digest": "808cd120fd3738eb2be1f6c6c029d98387b0e03fca7d1451e8fbf9c5ab3f643f"
},
{
"name": "crescent_moon",
"unicode": "1F319",
- "digest": "0959f838a410e8bfeebf00aa9658df56e515dbd2361142021071e17244662bfc"
+ "digest": "042e7e01e6e88b97a763b7cc41e2a2b3fe68a649bacf4a090cd28fc653baf640"
},
{
"name": "cricket",
"unicode": "1F3CF",
- "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3"
+ "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
},
{
"name": "cricket_bat_ball",
"unicode": "1F3CF",
- "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3"
+ "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
},
{
"name": "crocodile",
"unicode": "1F40A",
- "digest": "99abcb42264d40d2450aaca8c3759a019bfd600a311cf3027243f1ca200d4639"
+ "digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992"
},
{
"name": "cross",
"unicode": "271D",
- "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42"
+ "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
},
{
"name": "latin_cross",
"unicode": "271D",
- "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42"
+ "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
},
{
"name": "cross_heavy",
@@ -1902,157 +1902,157 @@
{
"name": "crossed_flags",
"unicode": "1F38C",
- "digest": "d4da057db289bec83f0106a94c89bd0cd9b52c7c7f8bc69bc8cbce480d53e12b"
+ "digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262"
},
{
"name": "crossed_swords",
"unicode": "2694",
- "digest": "f159978583fa77c73ba6de85d35c4195cbd55963e537bd2bfd8f98ab8ff3559a"
+ "digest": "3771a5b26b514236521ce44e15f7730fa9148c6a782b9b600ab870a1f7de6f9f"
},
{
"name": "crown",
"unicode": "1F451",
- "digest": "e6fe2a28b7d80749ca121cabbe89321dcecdd760a122e73fb1562ea9bb40e90d"
+ "digest": "6741e58d8f823194e0a3484ac1563e20d9e0b44c1bc46d82444dfffa092cdfc7"
},
{
"name": "cruise_ship",
"unicode": "1F6F3",
- "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd"
+ "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
},
{
"name": "passenger_ship",
"unicode": "1F6F3",
- "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd"
+ "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
},
{
"name": "cry",
"unicode": "1F622",
- "digest": "2d6a096796222c29b050f74db6b5aff9b9f61390c5eb56e45d1801918751002f"
+ "digest": "fc3307ec4fe75539770c1123a0e8e721d9e021009a502655132f68d7cc453816"
},
{
"name": "crying_cat_face",
"unicode": "1F63F",
- "digest": "df057d4e3e5c5c87caedf87ea3a6f936811b93f228f46bb7018d2bb5afaa6d35"
+ "digest": "4942c24935c22babdcb8af41d2c0a7588356b6b674bc238902e2f10ad03e2c5b"
},
{
"name": "crystal_ball",
"unicode": "1F52E",
- "digest": "7de438f88134c32c4db67d705e5fecf2a6187a87f56ebbb5bcc5ba09626e2935"
+ "digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30"
},
{
"name": "cupid",
"unicode": "1F498",
- "digest": "7cb3f7d1ddf9678982197ef0e65735fb465ae8e3652d611f37d3bcccf4d7e2c1"
+ "digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658"
},
{
"name": "curly_loop",
"unicode": "27B0",
- "digest": "881a43ae406cb74b2ef136bf970db9928bcdc3bbbb7393e90d2c597fe1dd9a96"
+ "digest": "9e4eb98d6597888f91208080c6a79824adb432ea34f46c85da26cb630bd1cc73"
},
{
"name": "currency_exchange",
"unicode": "1F4B1",
- "digest": "c4d76e9e61fac8d3c0cb9e07f1fbf1a7fcac6f4d4c78776ff7f04fc9391ce689"
+ "digest": "b85377265b9876888969aa42b65bba0be523a370175baf226f20131e535af554"
},
{
"name": "curry",
"unicode": "1F35B",
- "digest": "ebe41ee864c873e3a371888c0087b11dbcb124335812895002ed81fe2b6ba571"
+ "digest": "a01c0a713662817720b485f7739f57e61afc025f5c43792f4de961c94f92f31e"
},
{
"name": "custard",
"unicode": "1F36E",
- "digest": "afc192f405c30e2d529ec0f4b31a7faf474bcd01fded5294dc38880b8bb22155"
+ "digest": "85c2b9ac904134a6c3587eb0a0806f2ab4282c5ed5c79d41734f3203998f757e"
},
{
"name": "customs",
"unicode": "1F6C3",
- "digest": "5abb98151a79cebc1032c0ea149617093e42f41e50574a790a91074cabaa4c3a"
+ "digest": "eb2546e1e617d4c1a1f614318af5e5dacf3e8d9479ffa08108977defa83ded32"
},
{
"name": "cyclone",
"unicode": "1F300",
- "digest": "ae77e15bf2f312f03dbc5c7813d304005bbb549953482db9beb91810c585dc0e"
+ "digest": "7a0f8564d76adf2d0ed272f56dc0d01fb7b557852e0ca797e73f5472b8630bf3"
},
{
"name": "dagger",
"unicode": "1F5E1",
- "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976"
+ "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
},
{
"name": "dagger_knife",
"unicode": "1F5E1",
- "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976"
+ "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
},
{
"name": "dancer",
"unicode": "1F483",
- "digest": "e050db55afbb968e02219a58c7e82b824848d299a4df64f0d08d4e1872816203"
+ "digest": "66ffa86827e85acae4aa870c0859fe3a9dad03d21ff4bc800b61c95c902a8a90"
},
{
"name": "dancer_tone1",
"unicode": "1F483-1F3FB",
- "digest": "350f6b2e4589fdd436173163035621b8da0bd49c7b9ec9f39593aae5e0ed0641"
+ "digest": "bdbee740addc890e369d3469a3585eb0d1e4fbc7e04dd6f6aca762d8aeee6a8c"
},
{
"name": "dancer_tone2",
"unicode": "1F483-1F3FC",
- "digest": "a9efc84ec80582f286147ca34162a27fd5989f4030084acdbc309d4368660f5b"
+ "digest": "9f7b4c627241eaa2def9717a5286a423f0b9c1b044dd9ea4442a76f1858d14a4"
},
{
"name": "dancer_tone3",
"unicode": "1F483-1F3FD",
- "digest": "ef187f44278fdb8605c80f5cf199e0b3de8a49085dada2e215bb91e1d7d3be5d"
+ "digest": "a6bd49a377ce6c2004bf126b6f66d0b21d8c14103c2add7b10f12ed9e1c2d302"
},
{
"name": "dancer_tone4",
"unicode": "1F483-1F3FE",
- "digest": "5195bc352dc9d24cc5505a167c756038e55c05048c61799ea1bfdf2debe44ac2"
+ "digest": "4ec2a7629c01b0e9006b5cda4deae3bf297ce3b71d18063f93eeb5c14be19a1a"
},
{
"name": "dancer_tone5",
"unicode": "1F483-1F3FF",
- "digest": "55cb7eee9fa11a16a3932800a19e334546f7396df6aadde22e58fe3185926b16"
+ "digest": "2b48e3a6b366c6f55f73b816e6fb03c39e9890f586f7e9c9043cf0c013d9cdd5"
},
{
"name": "dancers",
"unicode": "1F46F",
- "digest": "39e7dfd9dafeee20f2968960b1179ee4bf3f2b63a3035fc1944024d0ae8b5de1"
+ "digest": "12be66ed19d232bb387270f40bece68bd0cb2342b318f6c9bb8b49c64ff7d0ad"
},
{
"name": "dango",
"unicode": "1F361",
- "digest": "2a1b50abe5dc72335344878d9b701028ccad651964d9e3affeedbf3c2bfd652a"
+ "digest": "34e8cd153c50f2d725abe8934c35c96a3ab533f0cc5fbb1e1474eafad1dc1fc2"
},
{
"name": "dark_sunglasses",
"unicode": "1F576",
- "digest": "6bb1e911a93d5eb0581d3ce8f8929125d3d8fc04e086f3263cfd25af1348ce6c"
+ "digest": "d0a735ad5bf0ece00af2a21abf950b89292ebd8ca6e28b1dbb1368252fb44afe"
},
{
"name": "dart",
"unicode": "1F3AF",
- "digest": "6f28741543a4c1eead21856128ffea1fcf772954fe6af40844dfde47f092ed32"
+ "digest": "998642f06a875905e0a6bf30963c025baff1cf55b8e76884b9119f2d71188b0c"
},
{
"name": "dash",
"unicode": "1F4A8",
- "digest": "25aef37611f1c2f2e96518bf8aeba80580dca9634c8505d390c147388adf6746"
+ "digest": "f7aae7d3887c67d76f3329c2dc9e6807dc580a4b07ab35599c7805e41823a345"
},
{
"name": "date",
"unicode": "1F4C5",
- "digest": "de591b8fad608be761b839beefe9e4c2316320bcf0c44c543a1bc4b89923d938"
+ "digest": "d0b695e4a7cfbbe71b4fbebf345b66ca98f0cf1c751362928e54c23ca78d4c7b"
},
{
"name": "deciduous_tree",
"unicode": "1F333",
- "digest": "ff31a52096ac1eae770f7f71b6d802198add2c8b4d9d7c9327071b6d6ab86c7b"
+ "digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790"
},
{
"name": "department_store",
"unicode": "1F3EC",
- "digest": "c1e200d5fdd792121acabdb17bbcfe8e28a63757cfd895c72d4909f14de95ac2"
+ "digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b"
},
{
"name": "descending_notes",
@@ -2062,17 +2062,17 @@
{
"name": "desert",
"unicode": "1F3DC",
- "digest": "e45815250bfc5411de516f87efa218874bcda4b0420b4c17182efc22ba0ce80d"
+ "digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e"
},
{
"name": "desktop",
"unicode": "1F5A5",
- "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de"
+ "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
},
{
"name": "desktop_computer",
"unicode": "1F5A5",
- "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de"
+ "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
},
{
"name": "desktop_window",
@@ -2082,47 +2082,47 @@
{
"name": "diamond_shape_with_a_dot_inside",
"unicode": "1F4A0",
- "digest": "4e0e6364b8682dec9a9e20676161c9c9c0faf0a5fdd5402ca2668b18f2bb850a"
+ "digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3"
},
{
"name": "diamonds",
"unicode": "2666",
- "digest": "42b13b2ed8e5fc63fbe81263c06cc203ba18a45ed5cc2a4fdbf617d219a0d3b4"
+ "digest": "bf3d9a020afe8aa226db73590bc193a9c2c3e6e642edd2445c5960c3e67cf153"
},
{
"name": "disappointed",
"unicode": "1F61E",
- "digest": "7f1a619fef84960a9f312d17a58aa58105a4f20a4072efb10227892ab22475d8"
+ "digest": "c0f406c6beea0fd1328adefc097d04aa16b72f7a5afa0867967d8ea25d72db17"
},
{
"name": "disappointed_relieved",
"unicode": "1F625",
- "digest": "a389f5e0a4b619dbc406217967fb1f8f3d0e49b3f790e554ae0ececadbf98967"
+ "digest": "c826f5dd4f2f7e5289d720851d4826ab8284d915606c1b152ab229b7fadbba14"
},
{
"name": "dividers",
"unicode": "1F5C2",
- "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668"
+ "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
},
{
"name": "card_index_dividers",
"unicode": "1F5C2",
- "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668"
+ "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
},
{
"name": "dizzy",
"unicode": "1F4AB",
- "digest": "d6fba9b906f0eabd46686e416273a2ca6634249374385f2abf7ed284f0eef995"
+ "digest": "d577545c2de42389695447c6ebbfef895f30f0fda84eef45684f9bf4a9c27ff1"
},
{
"name": "dizzy_face",
"unicode": "1F635",
- "digest": "b55e20c1551a2912bb5ec64a66c788c9d6f21594cc1da66032188f3814b03f40"
+ "digest": "7b3aeaffb4e15ccf633b91dda4a44847a1eb28d78ce58b4d171b20a771bde414"
},
{
"name": "do_not_litter",
"unicode": "1F6AF",
- "digest": "126f8c4085e0a8de8241f211f96c3f42c3e3400ea7d8fdf79a14443c3eceb972"
+ "digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb"
},
{
"name": "document",
@@ -2142,182 +2142,182 @@
{
"name": "dog",
"unicode": "1F436",
- "digest": "c7b729de8a0967b1f38c3fa5ded94e77e329588caeaaf43abfd1090f420e62bf"
+ "digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11"
},
{
"name": "dog2",
"unicode": "1F415",
- "digest": "e1897ca60bb3d2662cbe7933352e2b9c50739adf5901d3328797bf399575b97a"
+ "digest": "0a8901bce5ed994533ff84299b2a1364de28d872c9f9510d3426a83e8a9d2e34"
},
{
"name": "dollar",
"unicode": "1F4B5",
- "digest": "7db1e57f799439df1295d42b5249393f1e8cacc8df54caf30499c967a7282742"
+ "digest": "52438e38867aedc021740bb41f9ba336e75a50faa148419412a01d75d8c93155"
},
{
"name": "dolls",
"unicode": "1F38E",
- "digest": "398e7ff5780328700aadded7ce8c50757b1096af5cec66cc4d813a6714686b6d"
+ "digest": "a687184e9a0915deef44bb3cacfb19d3f3f19cf2c110f1da90191dd567333c57"
},
{
"name": "dolphin",
"unicode": "1F42C",
- "digest": "27385af08848d93acdd13f72751074c2cbccb5ab3c6047e334598af74ed4862d"
+ "digest": "0b7ee08f4236232ca533ed3a3023d28020d36f178efaec5ce8b0e13a84778512"
},
{
"name": "door",
"unicode": "1F6AA",
- "digest": "3365d7834086328ecbf1da0037f1cf1d0eb49534e173f7962a9e8f4b2ab87e26"
+ "digest": "984a9ca88852ebdb539e0c385d9c6ffe5010e9189bc372a3d00f5c8d44c8e6f5"
},
{
"name": "doughnut",
"unicode": "1F369",
- "digest": "b4b99fdfe8d07b49cbdd78f8c57e4424819a4ffc8a3ba4867da44cbb3b3a5cca"
+ "digest": "27634587e6a53807baa32157bb06b0e115c8ad8aefebba7ebb0b65a084170e3a"
},
{
"name": "dove",
"unicode": "1F54A",
- "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8"
+ "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
},
{
"name": "dove_of_peace",
"unicode": "1F54A",
- "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8"
+ "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
},
{
"name": "dragon",
"unicode": "1F409",
- "digest": "d7d016568b54d67017681a075fb799d4a2a790ecfa2946d02dbcee629eb4975d"
+ "digest": "2abcb3d945d848e34ffc76203b29ef26df7458856166fffd155611f7bbe72652"
},
{
"name": "dragon_face",
"unicode": "1F432",
- "digest": "4d0025f1df63b62448477a8f08a50704e15caafb10fea476b529113f41797ab9"
+ "digest": "0030548931b931e3b51f26cf660394aee36499e688ba83ce9cfccb635dcd4d54"
},
{
"name": "dress",
"unicode": "1F457",
- "digest": "02d56ed227280eaf5ad92830ee304afb81f74bb5a13c855397bcd04dd7fa51fb"
+ "digest": "96ceba928fb356f7c0ae99bf22552321f08a65d5f1c0340ab89641219ad366ad"
},
{
"name": "dromedary_camel",
"unicode": "1F42A",
- "digest": "5afe8a0b73f9f4560264020b1e02a566149dbc38c15a00d2fb5cd90b32d09a75"
+ "digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d"
},
{
"name": "droplet",
"unicode": "1F4A7",
- "digest": "a92c419792cbd3ba90ed21547362134cfac3e17a5304ee4e3872c9f7b561f834"
+ "digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3"
},
{
"name": "dvd",
"unicode": "1F4C0",
- "digest": "1ba23e2f01ced5e192e4c1d2f766d9bce400470e81c81410139fd3c0739422df"
+ "digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f"
},
{
"name": "e-mail",
"unicode": "1F4E7",
- "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb"
+ "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
},
{
"name": "email",
"unicode": "1F4E7",
- "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb"
+ "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
},
{
"name": "ear",
"unicode": "1F442",
- "digest": "70ba1103a34e68590d91a3b6f8acdbad3b1c65e46e31e26ee1cb855c1e21095e"
+ "digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8"
},
{
"name": "ear_of_rice",
"unicode": "1F33E",
- "digest": "ddd5f3cc83dbdafd9115861eecd0128e52165bb1dd0049df06ffc564b650d384"
+ "digest": "2997c340c2b333d6ba9b73f94ff1a1881735fe0cc4f0c72d7719b305499fc425"
},
{
"name": "ear_tone1",
"unicode": "1F442-1F3FB",
- "digest": "72977be94f5d287a09d175f98fba8b7955ae13aa12ce8e029c0ca875c02ee820"
+ "digest": "5ca759b8569a377a4e63e30d94b585b9f76d15348a8a0c1ba19fdc522790615e"
},
{
"name": "ear_tone2",
"unicode": "1F442-1F3FC",
- "digest": "5ff2e46cb3be7f13b8b94092246b58dab4c2a9ee2a5a46e0b84cf35a6928141f"
+ "digest": "12aafb3ef2cfcdc892b2877c2e24920620f0f77f850e12afbfe55eadce9e37df"
},
{
"name": "ear_tone3",
"unicode": "1F442-1F3FD",
- "digest": "19b523f5ada2acaea94b922059c458a3303f4da1dd4c197cf25d31a0e6ecc4b2"
+ "digest": "f4d28d9f72cf116ac92d80061eb84c918d6523bf53b2ad526f5457aba487d527"
},
{
"name": "ear_tone4",
"unicode": "1F442-1F3FE",
- "digest": "6a5cca9f49c539ef7d0883a2f39652f33ee2d3b25dca0234e4ba027ebbb2b466"
+ "digest": "eaa9453670f7e3adc6ec6934ee70efc9bf60fe6c99c5804b7ba9e3804aec65de"
},
{
"name": "ear_tone5",
"unicode": "1F442-1F3FF",
- "digest": "a0a56e8abd36e9be6e2448bcee6f56ecb8bf62d728b19ab6e8f9c6338e226b67"
+ "digest": "54bd0782419489556b80e9e0d15b05df74757aa4e04ba565f45c20d3dd60e3f1"
},
{
"name": "earth_africa",
"unicode": "1F30D",
- "digest": "d4921b543d7cf0c7344fa50c5e4d5a76c208d900be852adc1ee82ed4e8861a39"
+ "digest": "c691a6f591f5a07b268fd64efe113e81cec8d5963ad83ced2537422343ff7ecf"
},
{
"name": "earth_americas",
"unicode": "1F30E",
- "digest": "61691e6aa9b8d90fc7f75fbc6cc7add5c36022d38f3e05c9d7c54dc44cf865bb"
+ "digest": "a9c60cf8341ff59a9cc1a715b7144af734fcd28915a8e003a31ebf2abf9aedb1"
},
{
"name": "earth_asia",
"unicode": "1F30F",
- "digest": "262904cb552c7f5cf828a11071b3d430a74824b7464e8759ef93ee23b1705767"
+ "digest": "ee2beb61fb8c87279161c5a8c4ad17bb71ce790123f8fa33522941d027e060a5"
},
{
"name": "egg",
"unicode": "1F373",
- "digest": "a7dd617cad489c481ffd14937d9ed491cdd5756903e00473f42600c2fbefb600"
+ "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58"
},
{
"name": "eggplant",
"unicode": "1F346",
- "digest": "e5402e8ae5b7f9699ed86b97c242f7939d5731c5a364a2d5b9d04ea5d293cda1"
+ "digest": "ec0a460e0cf0e615f51279677594a899672e1b4ecd9396e17a8cfa2a3efe5238"
},
{
"name": "eight",
"unicode": "0038-20E3",
- "digest": "34e293d3228e4643a0132d592f96db91b651fe6ced056ac3c8a3fd49c5ed3416"
+ "digest": "57ff905033a32747690adba6486d12b09eb4d45de556f4e1ab6fb04e1fb861a8"
},
{
"name": "eight_pointed_black_star",
"unicode": "2734",
- "digest": "c3c2da75731a9a0f4f0a8d1f9cffef75c35e19b7f5d4081da33ac12b46be5fc2"
+ "digest": "7bf11f6e28591e3d0625296aaabf4ecb75c982e425abf3049339e93494acc17e"
},
{
"name": "eight_spoked_asterisk",
"unicode": "2733",
- "digest": "cc69618c1074d2b00e6f2c49df5e2c5ff6f4c0fae305505eb8c9daa65a0ea340"
+ "digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26"
},
{
"name": "electric_plug",
"unicode": "1F50C",
- "digest": "732e1d1675233a0b4643cb73d0c352f8a5a56a11ee90d26627ad1e43c2e4a8e5"
+ "digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0"
},
{
"name": "elephant",
"unicode": "1F418",
- "digest": "08df3910c4d5d8f49a72c47dd938195e495bde8fd8b3e7b17098a2c1afc41634"
+ "digest": "b7750f4b013fbd28ac5330e1694ef4d3b4a9c6fc7b807879db0c24b035a16c29"
},
{
"name": "end",
"unicode": "1F51A",
- "digest": "05844ab9dcb43deff86f04617af6ea09215595de1415dcfaae018bced57938fe"
+ "digest": "dd93aee6986eb637a8b58f234da47568b88525599f73246e322af030351997a2"
},
{
"name": "envelope",
"unicode": "2709",
- "digest": "aad272511d0db910437ba25cf1fb9c806d47aad92a232edb87055916daf4676a"
+ "digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78"
},
{
"name": "envelope_back",
@@ -2362,192 +2362,192 @@
{
"name": "envelope_with_arrow",
"unicode": "1F4E9",
- "digest": "c1ba19b5e7cf64c547ac46eee139e6af70700d49ab511a96e6828c30feb116bc"
+ "digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6"
},
{
"name": "euro",
"unicode": "1F4B6",
- "digest": "f571952583ffecfa5777065e4d1b680c423d25bc80e567a48fb5d7a1c1b5e735"
+ "digest": "3af3e223e8f26468a94f6f5c17198432656e8d20b3bab31566c2b5a86e717df4"
},
{
"name": "european_castle",
"unicode": "1F3F0",
- "digest": "db82e383975d079a7bb006e7868035088d75c33bd4031cf8466b71089b65426f"
+ "digest": "21082d0be7e3b2794e59ff0170da0cfe42a9b734cf02704603e3b52ff48202ba"
},
{
"name": "european_post_office",
"unicode": "1F3E4",
- "digest": "d9b38e0f0ca3ad8895b40c767bdbb2b142ccaf03a86c2f275f57a31ed478801a"
+ "digest": "02b4c7602939f0cb9cb2b4e05996bcdb6bd93cf8025c2ea02db8cbe13ca397d0"
},
{
"name": "evergreen_tree",
"unicode": "1F332",
- "digest": "60d8b2d86b20255341f7ecad6d0f178ba9db5fa6b3de92f1b439cdb19f2fc0b1"
+ "digest": "74b226098e66c0a94a92e0f22b9d631736e12dca72c34182c9d0ba56aa593172"
},
{
"name": "exclamation",
"unicode": "2757",
- "digest": "cd900ecf82de2b26f0d7783dac4b3232ae94d2cddad5bfacea2eaf65b7ac0a09"
+ "digest": "45b87ae4593656d7da49ff5645fb6a2a18d582553295358da9f09f1ae8272445"
},
{
"name": "expressionless",
"unicode": "1F611",
- "digest": "2ec9466b2d629907ce4c3e24e57f7ee556d2258ff011d972e14d0ae969a40c51"
+ "digest": "34e2a1c8121f4f0bc4ce33d226d8cc1a4ebf5260746df2b23e29eef24ee9372e"
},
{
"name": "eye",
"unicode": "1F441",
- "digest": "790841e8fce647173eec3c5019440ad9c7e916c535f92acb3132bd92df148cad"
+ "digest": "79ecff79c2edee630e72725b54e67ee2e96d24ca03fef2954a56a09c0a2227f8"
},
{
"name": "eye_in_speech_bubble",
"unicode": "1F441-1F5E8",
- "digest": "bcde5a89a7653bff302685d9d632dd2723796a7ac73125fb7b9493d1ca848e0a"
+ "digest": "c0050c026c2a3060723cab2df2603c1c7da7ed81faedb9ebe16cd89721928a55"
},
{
"name": "eyeglasses",
"unicode": "1F453",
- "digest": "fd140bef19c420bafe59368d35dd58a58a53e7145b104bae94be10f90679213b"
+ "digest": "d4a9585d6c43ef514a97c45c64607162e775a45544821f1470c6f8f25b93ab81"
},
{
"name": "eyes",
"unicode": "1F440",
- "digest": "57ed1f87ebe2485ea32ea69abdb8c5f7ccdcc149b33e74230d801f0883c68c5d"
+ "digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8"
},
{
"name": "factory",
"unicode": "1F3ED",
- "digest": "6e6b35ae013e5dd26852c9a95d05c39e89c1c1950a33f47e7b951c34af18f37c"
+ "digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0"
},
{
"name": "fallen_leaf",
"unicode": "1F342",
- "digest": "28ba8628065ffa973b525dd1455691c828d49c2b8c814af387880c13f6707f7e"
+ "digest": "81fce04231d48db0e55f3697f930e9a7e3306bed5e35f1234e98c40a24ac5626"
},
{
"name": "family",
"unicode": "1F46A",
- "digest": "b5307f86e54cfea581e8406f4b95c801e250a893a9d208cc9a69a6d910b90932"
+ "digest": "06f2ce63768ffe43b3d9b2a9660b34d043f37b3c91610dd62343ba21df8ecbe5"
},
{
"name": "family_mmb",
"unicode": "1F468-1F468-1F466",
- "digest": "49a753c3fcd4420800dd1cda585dae6bfa81615ad4862b477246456f86dc9e82"
+ "digest": "41a18405be796699a7eb7c36ab6f7d898e322749997f45387377acf5bb16a50f"
},
{
"name": "family_mmbb",
"unicode": "1F468-1F468-1F466-1F466",
- "digest": "882a3a0048efd666b0ab3a07b9f08041aa3a2acdab02664d0feff30bbfa70d68"
+ "digest": "87255d1d18c6971c8c083c818e598424c1bd717eed892478b7e9516639dbfb45"
},
{
"name": "family_mmg",
"unicode": "1F468-1F468-1F467",
- "digest": "45dd75c19d260a658c8ac93cf878976b96d2000f0efc9c59e72dacc80afb08fa"
+ "digest": "a132b1b8f10b318d8e23aee15dab4caa14528aeb3c89966d4bcc25fb54af72ad"
},
{
"name": "family_mmgb",
"unicode": "1F468-1F468-1F467-1F466",
- "digest": "910f44a348a951d36ee1f1484d237085bec5083c3875a4d908831dfc64530eaf"
+ "digest": "eb2bc1966df406aaf38ce5a58db9324162799cdacf31f74f40e6384807a8efc2"
},
{
"name": "family_mmgg",
"unicode": "1F468-1F468-1F467-1F467",
- "digest": "012e75ad0d1b16c2ce63bf80a1ebfb1fc194229cfaf1241039599b82832f6aee"
+ "digest": "24f3d60f98fbd6b687f7cacfb629390b90509a754036e5439ae5294759c0606b"
},
{
"name": "family_mwbb",
"unicode": "1F468-1F469-1F466-1F466",
- "digest": "049a32f61c54f093d2124e25f8b2ec7eac13161e2f2ebf6dc067797698cbe831"
+ "digest": "2f77692bcb9275c4df501b64a18401dcaf8c68b21f26fbdad59b1feab0c98fd1"
},
{
"name": "family_mwg",
"unicode": "1F468-1F469-1F467",
- "digest": "ba32c637caba634bda99ccba2a1a2a4b6f33aaaed933c30c7d5a51e8de1790d0"
+ "digest": "1a976d13127665d9386cebfdb24e5572dc499bda484c0ee05585886edc616130"
},
{
"name": "family_mwgb",
"unicode": "1F468-1F469-1F467-1F466",
- "digest": "198faba987f45429329b93bbce4f111329f284558bf0eecfa1424186b5f009fe"
+ "digest": "960ec2cbac13ef208e73644cd36711b83e6c070c36950f834f3669812839b7f8"
},
{
"name": "family_mwgg",
"unicode": "1F468-1F469-1F467-1F467",
- "digest": "3fa2e57cba314dcff04cf8186914823e1e081aabf34fa7437b05c58015df400c"
+ "digest": "8353b03dfa5c24aba75a0abdfdac01603f593819d54b4c7f2f88aafb31da0c6a"
},
{
"name": "family_wwb",
"unicode": "1F469-1F469-1F466",
- "digest": "b9592fc110a25a478569075deaa520308ef74579cd47aa44df9836599d68143f"
+ "digest": "07a5dd397718c553573689f6512f386729c13a12d5dc78be47c06405769cd98a"
},
{
"name": "family_wwbb",
"unicode": "1F469-1F469-1F466-1F466",
- "digest": "88f398997835fcf5153f17f6baf0deeb2a9c25ce2f8422192c18ac23e90b3193"
+ "digest": "b627f460f1da0d47b0b662402940b2b77c9538d380d05436dfca4b456c50c939"
},
{
"name": "family_wwg",
"unicode": "1F469-1F469-1F467",
- "digest": "c8d859d3c957fe0d535efccde295fe99bab76e3d28ab5a49c8e736608461cb2e"
+ "digest": "2d6f373bed53f1028f0fbe9caf036465a351f37b9e00fca7d722cc5a1984f251"
},
{
"name": "family_wwgb",
"unicode": "1F469-1F469-1F467-1F466",
- "digest": "006506e4a3d0c82642a0c8481ce95e5e3b969e20fe2def0a16dd686afddbc705"
+ "digest": "72be5c85e1621f73d6794edd6e428febdb366b9e4c816f7829897fd1ab34642b"
},
{
"name": "family_wwgg",
"unicode": "1F469-1F469-1F467-1F467",
- "digest": "2553f0deab133aad09b99411d9dd68b56fede30f55ee1f354358767765e36673"
+ "digest": "c39e0916069460d2d9741bddf58e76f5d6a09254cba0eeb262345adf8630bc32"
},
{
"name": "fast_forward",
"unicode": "23E9",
- "digest": "1baaed10969b60c083da754ee056bb71df36182cc65af40640acfb76f6b39200"
+ "digest": "e7d2d8085cfd406c2b096e8dd147dd3722290a5727b1f7df185989526a2335ec"
},
{
"name": "fax",
"unicode": "1F4E0",
- "digest": "b0a392192d03bd5d1ad5ee8eea933cf64725b1776819537bbed27561d78192e7"
+ "digest": "ff85ffa440c5379c9b138ebe2d7912d6098da3b37a051b80442d5557b7f993b0"
},
{
"name": "fearful",
"unicode": "1F628",
- "digest": "7c4cc4de3357c2a6d6e779342b09dabb3ef832a32f2778a0ba074b446f588e8f"
+ "digest": "b72bdf7d075d5c4e38bbd8512fb45fda2e85c9c8732a47e67575ae9f2ed4c5df"
},
{
"name": "feet",
"unicode": "1F43E",
- "digest": "cae13fb54ec64dbcf86ea25bebe2b79877e2d4f5d810b867f095f1d3dfc7f144"
+ "digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016"
},
{
"name": "ferris_wheel",
"unicode": "1F3A1",
- "digest": "a710a8a0fb039d953313b75330db37e3228d856593547b1f04dc83c00168b987"
+ "digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c"
},
{
"name": "ferry",
"unicode": "26F4",
- "digest": "21ea239b5adb68dc1ce6c5a1993b0a0b835ef6cc7a0a27cb890838d8475504f6"
+ "digest": "5002a72af2e3c4cef9a36ad5987aeed7d99f96bfd13e56f78957315ec7e749a3"
},
{
"name": "field_hockey",
"unicode": "1F3D1",
- "digest": "1e46c7f0b5b79c90a5d211ea14cd7e358b1a26a3c8294439253f2b08d0e5c92e"
+ "digest": "4ee091d96161ba719ab8fd6f2b03f96d902a6f22cffe0563b930618bb8ac2b67"
},
{
"name": "file_cabinet",
"unicode": "1F5C4",
- "digest": "c0b7bdab6c98909eb0fbf1ac89da0008bb00ddb1cb57fe64b4a5ac993eeb18c9"
+ "digest": "92914147bf93e6d64271ff99d217a18a9850a367d08a5f9f458ecf9311a5bbe9"
},
{
"name": "file_folder",
"unicode": "1F4C1",
- "digest": "d98f93c6d7283df0c45f08d3d31ecf5b91b6db1b735959f19e42bfada500a0d1"
+ "digest": "62a42a929267cfbfdb795ead381c9657c343458bc5fca95ea8a0ab892c61d4f6"
},
{
"name": "film_frames",
"unicode": "1F39E",
- "digest": "754a0a60e978f8299a0c4f8959e1f9260f01683e15ae943db430036f01a79b18"
+ "digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a"
},
{
"name": "finger_pointing_down",
@@ -2602,17 +2602,17 @@
{
"name": "fire",
"unicode": "1F525",
- "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641"
+ "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
},
{
"name": "flame",
"unicode": "1F525",
- "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641"
+ "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
},
{
"name": "fire_engine",
"unicode": "1F692",
- "digest": "3ae03fa34a7088ada95458eb4ee3e97691b3489149f6bbc168086f0483ed3bb2"
+ "digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20"
},
{
"name": "fire_engine_oncoming",
@@ -2627,507 +2627,507 @@
{
"name": "fireworks",
"unicode": "1F386",
- "digest": "3dee83a27c406960253ca1460eb88a599c7b81506051b69605a421b17fe8282c"
+ "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65"
},
{
"name": "first_quarter_moon",
"unicode": "1F313",
- "digest": "8fa066362d77bd889090bbe0904ca47f34704e29781c67133c6eaa521c3e1972"
+ "digest": "a207ce93084448622a4a5c49c85c566a9fda6be7337c86a013eeb713fe47fd29"
},
{
"name": "first_quarter_moon_with_face",
"unicode": "1F31B",
- "digest": "8877edb366f8eaa00fd83200acf5a17c3b84d246a250519d565dda3aea866ec3"
+ "digest": "1d1f54a5075f2311bcc017c44898b9d8c58edc13b298d58c238fff9ab8ee2ef3"
},
{
"name": "fish",
"unicode": "1F41F",
- "digest": "9ce742108794cc15e59f7719623ae938efbd8155c93ad72585a32f4e32ea9414"
+ "digest": "8f62f08fbeaf39694c19816b5c7d4f292017fe5bf9f8dd7e40f1630f5f83b28b"
},
{
"name": "fish_cake",
"unicode": "1F365",
- "digest": "1b5b14509287e30da9b8d7abcec376b247f9095aea4bf3fc320349f061a4c321"
+ "digest": "5a6ca2100c8830927b22afa6f1d2fc821f5692cd23507fe5a776f6e085cbbfb2"
},
{
"name": "fishing_pole_and_fish",
"unicode": "1F3A3",
- "digest": "35db56776db1fcec7c8479922d57d54da2577cfe44a894bfd78c51c950c450fb"
+ "digest": "f8fb84eccceec88321b0a2a46f732ecfc378f787c19c27ac1327735f1ca9a48b"
},
{
"name": "fist",
"unicode": "270A",
- "digest": "6b80ac2e4d8b830ae06f7c1626d456460094e4ba20c20fb82dabb6b3d2ce7605"
+ "digest": "557f96d85615b8d78436bc67266115bfc8556c97c14f7909dfda1cf134e8344f"
},
{
"name": "fist_tone1",
"unicode": "270A-1F3FB",
- "digest": "d7c79f4f988dd68f064baa5a3a568ab299f8d409db45c8463f39b80e5dd6081f"
+ "digest": "6c1b946f9e01abc39b5085e24e8b6077fc0e34188e8daa30c6a3adddd387413e"
},
{
"name": "fist_tone2",
"unicode": "270A-1F3FC",
- "digest": "d1108194e2d962f9ccd00131876d769a8e003117a460d18b2ccbf93e0a0ea346"
+ "digest": "e9b9e1ec638dca4d5e1519bca7338f58cce2f2a282ee4c3581e8643166fc415f"
},
{
"name": "fist_tone3",
"unicode": "270A-1F3FD",
- "digest": "12f5644b632c95a5c2e41cc9af299e286e266db8b3860091ef5be5f0c4ccc026"
+ "digest": "8c14d24055c143960b3d2a27fe23c55d2d3ac5f84f87e4e876616235e8698c7f"
},
{
"name": "fist_tone4",
"unicode": "270A-1F3FE",
- "digest": "521a3ac573381f3bc37a08ddd2d122767aaa0b6b7a38050d3671a12343351816"
+ "digest": "923f034f481e952e6e5d1664588f99f79bd5416d4197b0ade6621f2669ce5765"
},
{
"name": "fist_tone5",
"unicode": "270A-1F3FF",
- "digest": "604e5a234da1b9160e506b3c9026faf9e04268fced7b44baa1ef5e3d4efa83a4"
+ "digest": "d691d2902216080916a29047e07d7a5bf2aed07e062067ca9d01cbf6fdf48c8d"
},
{
"name": "five",
"unicode": "0035-20E3",
- "digest": "0cbd6cd11eb6c2d67749112750d125f4f0a07b53bb7bfb1de0986d943ea9d632"
+ "digest": "8f03f62fdbf744ae49c8a60fbf715ebfccbd6b62d91148e0923907006f3c2726"
},
{
"name": "flag_ac",
"unicode": "1F1E6-1F1E8",
- "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da"
+ "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
},
{
"name": "ac",
"unicode": "1F1E6-1F1E8",
- "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da"
+ "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
},
{
"name": "flag_ad",
"unicode": "1F1E6-1F1E9",
- "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952"
+ "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
},
{
"name": "ad",
"unicode": "1F1E6-1F1E9",
- "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952"
+ "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
},
{
"name": "flag_ae",
"unicode": "1F1E6-1F1EA",
- "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd"
+ "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
},
{
"name": "ae",
"unicode": "1F1E6-1F1EA",
- "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd"
+ "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
},
{
"name": "flag_af",
"unicode": "1F1E6-1F1EB",
- "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226"
+ "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
},
{
"name": "af",
"unicode": "1F1E6-1F1EB",
- "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226"
+ "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
},
{
"name": "flag_ag",
"unicode": "1F1E6-1F1EC",
- "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4"
+ "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
},
{
"name": "ag",
"unicode": "1F1E6-1F1EC",
- "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4"
+ "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
},
{
"name": "flag_ai",
"unicode": "1F1E6-1F1EE",
- "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2"
+ "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
},
{
"name": "ai",
"unicode": "1F1E6-1F1EE",
- "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2"
+ "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
},
{
"name": "flag_al",
"unicode": "1F1E6-1F1F1",
- "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847"
+ "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
},
{
"name": "al",
"unicode": "1F1E6-1F1F1",
- "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847"
+ "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
},
{
"name": "flag_am",
"unicode": "1F1E6-1F1F2",
- "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d"
+ "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
},
{
"name": "am",
"unicode": "1F1E6-1F1F2",
- "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d"
+ "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
},
{
"name": "flag_ao",
"unicode": "1F1E6-1F1F4",
- "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187"
+ "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
},
{
"name": "ao",
"unicode": "1F1E6-1F1F4",
- "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187"
+ "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
},
{
"name": "flag_aq",
"unicode": "1F1E6-1F1F6",
- "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616"
+ "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
},
{
"name": "aq",
"unicode": "1F1E6-1F1F6",
- "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616"
+ "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
},
{
"name": "flag_ar",
"unicode": "1F1E6-1F1F7",
- "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3"
+ "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
},
{
"name": "ar",
"unicode": "1F1E6-1F1F7",
- "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3"
+ "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
},
{
"name": "flag_as",
"unicode": "1F1E6-1F1F8",
- "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141"
+ "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
},
{
"name": "as",
"unicode": "1F1E6-1F1F8",
- "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141"
+ "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
},
{
"name": "flag_at",
"unicode": "1F1E6-1F1F9",
- "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba"
+ "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
},
{
"name": "at",
"unicode": "1F1E6-1F1F9",
- "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba"
+ "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
},
{
"name": "flag_au",
"unicode": "1F1E6-1F1FA",
- "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3"
+ "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
},
{
"name": "au",
"unicode": "1F1E6-1F1FA",
- "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3"
+ "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
},
{
"name": "flag_aw",
"unicode": "1F1E6-1F1FC",
- "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8"
+ "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
},
{
"name": "aw",
"unicode": "1F1E6-1F1FC",
- "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8"
+ "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
},
{
"name": "flag_ax",
"unicode": "1F1E6-1F1FD",
- "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1"
+ "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
},
{
"name": "ax",
"unicode": "1F1E6-1F1FD",
- "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1"
+ "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
},
{
"name": "flag_az",
"unicode": "1F1E6-1F1FF",
- "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b"
+ "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
},
{
"name": "az",
"unicode": "1F1E6-1F1FF",
- "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b"
+ "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
},
{
"name": "flag_ba",
"unicode": "1F1E7-1F1E6",
- "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a"
+ "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
},
{
"name": "ba",
"unicode": "1F1E7-1F1E6",
- "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a"
+ "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
},
{
"name": "flag_bb",
"unicode": "1F1E7-1F1E7",
- "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512"
+ "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
},
{
"name": "bb",
"unicode": "1F1E7-1F1E7",
- "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512"
+ "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
},
{
"name": "flag_bd",
"unicode": "1F1E7-1F1E9",
- "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18"
+ "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
},
{
"name": "bd",
"unicode": "1F1E7-1F1E9",
- "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18"
+ "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
},
{
"name": "flag_be",
"unicode": "1F1E7-1F1EA",
- "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950"
+ "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
},
{
"name": "be",
"unicode": "1F1E7-1F1EA",
- "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950"
+ "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
},
{
"name": "flag_bf",
"unicode": "1F1E7-1F1EB",
- "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5"
+ "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
},
{
"name": "bf",
"unicode": "1F1E7-1F1EB",
- "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5"
+ "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
},
{
"name": "flag_bg",
"unicode": "1F1E7-1F1EC",
- "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95"
+ "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
},
{
"name": "bg",
"unicode": "1F1E7-1F1EC",
- "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95"
+ "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
},
{
"name": "flag_bh",
"unicode": "1F1E7-1F1ED",
- "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de"
+ "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
},
{
"name": "bh",
"unicode": "1F1E7-1F1ED",
- "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de"
+ "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
},
{
"name": "flag_bi",
"unicode": "1F1E7-1F1EE",
- "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b"
+ "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
},
{
"name": "bi",
"unicode": "1F1E7-1F1EE",
- "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b"
+ "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
},
{
"name": "flag_bj",
"unicode": "1F1E7-1F1EF",
- "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509"
+ "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
},
{
"name": "bj",
"unicode": "1F1E7-1F1EF",
- "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509"
+ "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
},
{
"name": "flag_bl",
"unicode": "1F1E7-1F1F1",
- "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14"
+ "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
},
{
"name": "bl",
"unicode": "1F1E7-1F1F1",
- "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14"
+ "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
},
{
"name": "flag_black",
"unicode": "1F3F4",
- "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6"
+ "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
},
{
"name": "waving_black_flag",
"unicode": "1F3F4",
- "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6"
+ "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
},
{
"name": "flag_bm",
"unicode": "1F1E7-1F1F2",
- "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a"
+ "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
},
{
"name": "bm",
"unicode": "1F1E7-1F1F2",
- "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a"
+ "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
},
{
"name": "flag_bn",
"unicode": "1F1E7-1F1F3",
- "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a"
+ "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
},
{
"name": "bn",
"unicode": "1F1E7-1F1F3",
- "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a"
+ "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
},
{
"name": "flag_bo",
"unicode": "1F1E7-1F1F4",
- "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c"
+ "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
},
{
"name": "bo",
"unicode": "1F1E7-1F1F4",
- "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c"
+ "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
},
{
"name": "flag_bq",
"unicode": "1F1E7-1F1F6",
- "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171"
+ "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
},
{
"name": "bq",
"unicode": "1F1E7-1F1F6",
- "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171"
+ "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
},
{
"name": "flag_br",
"unicode": "1F1E7-1F1F7",
- "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec"
+ "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
},
{
"name": "br",
"unicode": "1F1E7-1F1F7",
- "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec"
+ "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
},
{
"name": "flag_bs",
"unicode": "1F1E7-1F1F8",
- "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f"
+ "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
},
{
"name": "bs",
"unicode": "1F1E7-1F1F8",
- "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f"
+ "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
},
{
"name": "flag_bt",
"unicode": "1F1E7-1F1F9",
- "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12"
+ "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
},
{
"name": "bt",
"unicode": "1F1E7-1F1F9",
- "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12"
+ "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
},
{
"name": "flag_bv",
"unicode": "1F1E7-1F1FB",
- "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41"
+ "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
{
"name": "bv",
"unicode": "1F1E7-1F1FB",
- "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41"
+ "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
{
"name": "flag_bw",
"unicode": "1F1E7-1F1FC",
- "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d"
+ "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
},
{
"name": "bw",
"unicode": "1F1E7-1F1FC",
- "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d"
+ "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
},
{
"name": "flag_by",
"unicode": "1F1E7-1F1FE",
- "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d"
+ "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
},
{
"name": "by",
"unicode": "1F1E7-1F1FE",
- "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d"
+ "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
},
{
"name": "flag_bz",
"unicode": "1F1E7-1F1FF",
- "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea"
+ "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
},
{
"name": "bz",
"unicode": "1F1E7-1F1FF",
- "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea"
+ "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
},
{
"name": "flag_ca",
"unicode": "1F1E8-1F1E6",
- "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796"
+ "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
},
{
"name": "ca",
"unicode": "1F1E8-1F1E6",
- "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796"
+ "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
},
{
"name": "flag_cc",
"unicode": "1F1E8-1F1E8",
- "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b"
+ "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
},
{
"name": "cc",
"unicode": "1F1E8-1F1E8",
- "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b"
+ "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
},
{
"name": "flag_cd",
"unicode": "1F1E8-1F1E9",
- "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066"
+ "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
},
{
"name": "congo",
"unicode": "1F1E8-1F1E9",
- "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066"
+ "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
},
{
"name": "flag_cf",
"unicode": "1F1E8-1F1EB",
- "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce"
+ "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
},
{
"name": "cf",
"unicode": "1F1E8-1F1EB",
- "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce"
+ "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
},
{
"name": "flag_cg",
"unicode": "1F1E8-1F1EC",
- "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da"
+ "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
},
{
"name": "cg",
"unicode": "1F1E8-1F1EC",
- "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da"
+ "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
},
{
"name": "flag_ch",
@@ -3142,2162 +3142,2162 @@
{
"name": "flag_ci",
"unicode": "1F1E8-1F1EE",
- "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a"
+ "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
},
{
"name": "ci",
"unicode": "1F1E8-1F1EE",
- "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a"
+ "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
},
{
"name": "flag_ck",
"unicode": "1F1E8-1F1F0",
- "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c"
+ "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
},
{
"name": "ck",
"unicode": "1F1E8-1F1F0",
- "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c"
+ "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
},
{
"name": "flag_cl",
"unicode": "1F1E8-1F1F1",
- "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f"
+ "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
},
{
"name": "chile",
"unicode": "1F1E8-1F1F1",
- "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f"
+ "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
},
{
"name": "flag_cm",
"unicode": "1F1E8-1F1F2",
- "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e"
+ "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
},
{
"name": "cm",
"unicode": "1F1E8-1F1F2",
- "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e"
+ "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
},
{
"name": "flag_cn",
"unicode": "1F1E8-1F1F3",
- "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a"
+ "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
},
{
"name": "cn",
"unicode": "1F1E8-1F1F3",
- "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a"
+ "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
},
{
"name": "flag_co",
"unicode": "1F1E8-1F1F4",
- "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151"
+ "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
},
{
"name": "co",
"unicode": "1F1E8-1F1F4",
- "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151"
+ "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
},
{
"name": "flag_cp",
"unicode": "1F1E8-1F1F5",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "cp",
"unicode": "1F1E8-1F1F5",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "flag_cr",
"unicode": "1F1E8-1F1F7",
- "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4"
+ "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
},
{
"name": "cr",
"unicode": "1F1E8-1F1F7",
- "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4"
+ "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
},
{
"name": "flag_cu",
"unicode": "1F1E8-1F1FA",
- "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83"
+ "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
},
{
"name": "cu",
"unicode": "1F1E8-1F1FA",
- "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83"
+ "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
},
{
"name": "flag_cv",
"unicode": "1F1E8-1F1FB",
- "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a"
+ "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
},
{
"name": "cv",
"unicode": "1F1E8-1F1FB",
- "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a"
+ "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
},
{
"name": "flag_cw",
"unicode": "1F1E8-1F1FC",
- "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411"
+ "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
},
{
"name": "cw",
"unicode": "1F1E8-1F1FC",
- "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411"
+ "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
},
{
"name": "flag_cx",
"unicode": "1F1E8-1F1FD",
- "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7"
+ "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
},
{
"name": "cx",
"unicode": "1F1E8-1F1FD",
- "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7"
+ "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
},
{
"name": "flag_cy",
"unicode": "1F1E8-1F1FE",
- "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b"
+ "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
},
{
"name": "cy",
"unicode": "1F1E8-1F1FE",
- "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b"
+ "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
},
{
"name": "flag_cz",
"unicode": "1F1E8-1F1FF",
- "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4"
+ "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
},
{
"name": "cz",
"unicode": "1F1E8-1F1FF",
- "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4"
+ "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
},
{
"name": "flag_de",
"unicode": "1F1E9-1F1EA",
- "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce"
+ "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
},
{
"name": "de",
"unicode": "1F1E9-1F1EA",
- "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce"
+ "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
},
{
"name": "flag_dg",
"unicode": "1F1E9-1F1EC",
- "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d"
+ "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
{
"name": "dg",
"unicode": "1F1E9-1F1EC",
- "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d"
+ "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
{
"name": "flag_dj",
"unicode": "1F1E9-1F1EF",
- "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6"
+ "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
},
{
"name": "dj",
"unicode": "1F1E9-1F1EF",
- "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6"
+ "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
},
{
"name": "flag_dk",
"unicode": "1F1E9-1F1F0",
- "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c"
+ "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
},
{
"name": "dk",
"unicode": "1F1E9-1F1F0",
- "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c"
+ "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
},
{
"name": "flag_dm",
"unicode": "1F1E9-1F1F2",
- "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c"
+ "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
},
{
"name": "dm",
"unicode": "1F1E9-1F1F2",
- "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c"
+ "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
},
{
"name": "flag_do",
"unicode": "1F1E9-1F1F4",
- "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230"
+ "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
},
{
"name": "do",
"unicode": "1F1E9-1F1F4",
- "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230"
+ "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
},
{
"name": "flag_dz",
"unicode": "1F1E9-1F1FF",
- "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618"
+ "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
},
{
"name": "dz",
"unicode": "1F1E9-1F1FF",
- "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618"
+ "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
},
{
"name": "flag_ea",
"unicode": "1F1EA-1F1E6",
- "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002"
+ "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
},
{
"name": "ea",
"unicode": "1F1EA-1F1E6",
- "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002"
+ "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
},
{
"name": "flag_ec",
"unicode": "1F1EA-1F1E8",
- "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd"
+ "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
},
{
"name": "ec",
"unicode": "1F1EA-1F1E8",
- "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd"
+ "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
},
{
"name": "flag_ee",
"unicode": "1F1EA-1F1EA",
- "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48"
+ "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
},
{
"name": "ee",
"unicode": "1F1EA-1F1EA",
- "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48"
+ "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
},
{
"name": "flag_eg",
"unicode": "1F1EA-1F1EC",
- "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4"
+ "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
},
{
"name": "eg",
"unicode": "1F1EA-1F1EC",
- "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4"
+ "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
},
{
"name": "flag_eh",
"unicode": "1F1EA-1F1ED",
- "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b"
+ "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
},
{
"name": "eh",
"unicode": "1F1EA-1F1ED",
- "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b"
+ "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
},
{
"name": "flag_er",
"unicode": "1F1EA-1F1F7",
- "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6"
+ "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
},
{
"name": "er",
"unicode": "1F1EA-1F1F7",
- "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6"
+ "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
},
{
"name": "flag_es",
"unicode": "1F1EA-1F1F8",
- "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c"
+ "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
},
{
"name": "es",
"unicode": "1F1EA-1F1F8",
- "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c"
+ "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
},
{
"name": "flag_et",
"unicode": "1F1EA-1F1F9",
- "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de"
+ "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
},
{
"name": "et",
"unicode": "1F1EA-1F1F9",
- "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de"
+ "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
},
{
"name": "flag_eu",
"unicode": "1F1EA-1F1FA",
- "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74"
+ "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
},
{
"name": "eu",
"unicode": "1F1EA-1F1FA",
- "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74"
+ "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
},
{
"name": "flag_fi",
"unicode": "1F1EB-1F1EE",
- "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e"
+ "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
},
{
"name": "fi",
"unicode": "1F1EB-1F1EE",
- "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e"
+ "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
},
{
"name": "flag_fj",
"unicode": "1F1EB-1F1EF",
- "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78"
+ "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
},
{
"name": "fj",
"unicode": "1F1EB-1F1EF",
- "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78"
+ "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
},
{
"name": "flag_fk",
"unicode": "1F1EB-1F1F0",
- "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8"
+ "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
},
{
"name": "fk",
"unicode": "1F1EB-1F1F0",
- "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8"
+ "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
},
{
"name": "flag_fm",
"unicode": "1F1EB-1F1F2",
- "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e"
+ "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
},
{
"name": "fm",
"unicode": "1F1EB-1F1F2",
- "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e"
+ "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
},
{
"name": "flag_fo",
"unicode": "1F1EB-1F1F4",
- "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752"
+ "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
},
{
"name": "fo",
"unicode": "1F1EB-1F1F4",
- "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752"
+ "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
},
{
"name": "flag_fr",
"unicode": "1F1EB-1F1F7",
- "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee"
+ "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
},
{
"name": "fr",
"unicode": "1F1EB-1F1F7",
- "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee"
+ "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
},
{
"name": "flag_ga",
"unicode": "1F1EC-1F1E6",
- "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326"
+ "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
},
{
"name": "ga",
"unicode": "1F1EC-1F1E6",
- "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326"
+ "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
},
{
"name": "flag_gb",
"unicode": "1F1EC-1F1E7",
- "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0"
+ "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
},
{
"name": "gb",
"unicode": "1F1EC-1F1E7",
- "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0"
+ "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
},
{
"name": "flag_gd",
"unicode": "1F1EC-1F1E9",
- "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081"
+ "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
},
{
"name": "gd",
"unicode": "1F1EC-1F1E9",
- "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081"
+ "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
},
{
"name": "flag_ge",
"unicode": "1F1EC-1F1EA",
- "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b"
+ "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
},
{
"name": "ge",
"unicode": "1F1EC-1F1EA",
- "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b"
+ "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
},
{
"name": "flag_gf",
"unicode": "1F1EC-1F1EB",
- "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d"
+ "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
},
{
"name": "gf",
"unicode": "1F1EC-1F1EB",
- "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d"
+ "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
},
{
"name": "flag_gg",
"unicode": "1F1EC-1F1EC",
- "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66"
+ "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
},
{
"name": "gg",
"unicode": "1F1EC-1F1EC",
- "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66"
+ "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
},
{
"name": "flag_gh",
"unicode": "1F1EC-1F1ED",
- "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b"
+ "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
},
{
"name": "gh",
"unicode": "1F1EC-1F1ED",
- "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b"
+ "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
},
{
"name": "flag_gi",
"unicode": "1F1EC-1F1EE",
- "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649"
+ "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
},
{
"name": "gi",
"unicode": "1F1EC-1F1EE",
- "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649"
+ "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
},
{
"name": "flag_gl",
"unicode": "1F1EC-1F1F1",
- "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec"
+ "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
},
{
"name": "gl",
"unicode": "1F1EC-1F1F1",
- "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec"
+ "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
},
{
"name": "flag_gm",
"unicode": "1F1EC-1F1F2",
- "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d"
+ "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
},
{
"name": "gm",
"unicode": "1F1EC-1F1F2",
- "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d"
+ "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
},
{
"name": "flag_gn",
"unicode": "1F1EC-1F1F3",
- "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8"
+ "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
},
{
"name": "gn",
"unicode": "1F1EC-1F1F3",
- "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8"
+ "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
},
{
"name": "flag_gp",
"unicode": "1F1EC-1F1F5",
- "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96"
+ "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
},
{
"name": "gp",
"unicode": "1F1EC-1F1F5",
- "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96"
+ "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
},
{
"name": "flag_gq",
"unicode": "1F1EC-1F1F6",
- "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41"
+ "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
},
{
"name": "gq",
"unicode": "1F1EC-1F1F6",
- "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41"
+ "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
},
{
"name": "flag_gr",
"unicode": "1F1EC-1F1F7",
- "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b"
+ "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
},
{
"name": "gr",
"unicode": "1F1EC-1F1F7",
- "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b"
+ "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
},
{
"name": "flag_gs",
"unicode": "1F1EC-1F1F8",
- "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c"
+ "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
},
{
"name": "gs",
"unicode": "1F1EC-1F1F8",
- "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c"
+ "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
},
{
"name": "flag_gt",
"unicode": "1F1EC-1F1F9",
- "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8"
+ "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
},
{
"name": "gt",
"unicode": "1F1EC-1F1F9",
- "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8"
+ "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
},
{
"name": "flag_gu",
"unicode": "1F1EC-1F1FA",
- "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb"
+ "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
},
{
"name": "gu",
"unicode": "1F1EC-1F1FA",
- "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb"
+ "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
},
{
"name": "flag_gw",
"unicode": "1F1EC-1F1FC",
- "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6"
+ "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
},
{
"name": "gw",
"unicode": "1F1EC-1F1FC",
- "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6"
+ "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
},
{
"name": "flag_gy",
"unicode": "1F1EC-1F1FE",
- "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be"
+ "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
},
{
"name": "gy",
"unicode": "1F1EC-1F1FE",
- "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be"
+ "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
},
{
"name": "flag_hk",
"unicode": "1F1ED-1F1F0",
- "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe"
+ "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
},
{
"name": "hk",
"unicode": "1F1ED-1F1F0",
- "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe"
+ "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
},
{
"name": "flag_hm",
"unicode": "1F1ED-1F1F2",
- "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc"
+ "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
},
{
"name": "hm",
"unicode": "1F1ED-1F1F2",
- "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc"
+ "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
},
{
"name": "flag_hn",
"unicode": "1F1ED-1F1F3",
- "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9"
+ "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
},
{
"name": "hn",
"unicode": "1F1ED-1F1F3",
- "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9"
+ "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
},
{
"name": "flag_hr",
"unicode": "1F1ED-1F1F7",
- "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50"
+ "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
},
{
"name": "hr",
"unicode": "1F1ED-1F1F7",
- "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50"
+ "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
},
{
"name": "flag_ht",
"unicode": "1F1ED-1F1F9",
- "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059"
+ "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
},
{
"name": "ht",
"unicode": "1F1ED-1F1F9",
- "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059"
+ "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
},
{
"name": "flag_hu",
"unicode": "1F1ED-1F1FA",
- "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8"
+ "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
},
{
"name": "hu",
"unicode": "1F1ED-1F1FA",
- "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8"
+ "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
},
{
"name": "flag_ic",
"unicode": "1F1EE-1F1E8",
- "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3"
+ "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
},
{
"name": "ic",
"unicode": "1F1EE-1F1E8",
- "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3"
+ "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
},
{
"name": "flag_id",
"unicode": "1F1EE-1F1E9",
- "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f"
+ "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
},
{
"name": "indonesia",
"unicode": "1F1EE-1F1E9",
- "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f"
+ "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
},
{
"name": "flag_ie",
"unicode": "1F1EE-1F1EA",
- "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3"
+ "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
},
{
"name": "ie",
"unicode": "1F1EE-1F1EA",
- "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3"
+ "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
},
{
"name": "flag_il",
"unicode": "1F1EE-1F1F1",
- "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f"
+ "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
},
{
"name": "il",
"unicode": "1F1EE-1F1F1",
- "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f"
+ "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
},
{
"name": "flag_im",
"unicode": "1F1EE-1F1F2",
- "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc"
+ "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
},
{
"name": "im",
"unicode": "1F1EE-1F1F2",
- "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc"
+ "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
},
{
"name": "flag_in",
"unicode": "1F1EE-1F1F3",
- "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b"
+ "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
},
{
"name": "in",
"unicode": "1F1EE-1F1F3",
- "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b"
+ "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
},
{
"name": "flag_io",
"unicode": "1F1EE-1F1F4",
- "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c"
+ "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
{
"name": "io",
"unicode": "1F1EE-1F1F4",
- "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c"
+ "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
{
"name": "flag_iq",
"unicode": "1F1EE-1F1F6",
- "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3"
+ "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
},
{
"name": "iq",
"unicode": "1F1EE-1F1F6",
- "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3"
+ "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
},
{
"name": "flag_ir",
"unicode": "1F1EE-1F1F7",
- "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85"
+ "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
},
{
"name": "ir",
"unicode": "1F1EE-1F1F7",
- "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85"
+ "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
},
{
"name": "flag_is",
"unicode": "1F1EE-1F1F8",
- "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268"
+ "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
},
{
"name": "is",
"unicode": "1F1EE-1F1F8",
- "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268"
+ "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
},
{
"name": "flag_it",
"unicode": "1F1EE-1F1F9",
- "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0"
+ "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
},
{
"name": "it",
"unicode": "1F1EE-1F1F9",
- "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0"
+ "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
},
{
"name": "flag_je",
"unicode": "1F1EF-1F1EA",
- "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c"
+ "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
},
{
"name": "je",
"unicode": "1F1EF-1F1EA",
- "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c"
+ "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
},
{
"name": "flag_jm",
"unicode": "1F1EF-1F1F2",
- "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f"
+ "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
},
{
"name": "jm",
"unicode": "1F1EF-1F1F2",
- "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f"
+ "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
},
{
"name": "flag_jo",
"unicode": "1F1EF-1F1F4",
- "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d"
+ "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
},
{
"name": "jo",
"unicode": "1F1EF-1F1F4",
- "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d"
+ "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
},
{
"name": "flag_jp",
"unicode": "1F1EF-1F1F5",
- "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3"
+ "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
},
{
"name": "jp",
"unicode": "1F1EF-1F1F5",
- "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3"
+ "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
},
{
"name": "flag_ke",
"unicode": "1F1F0-1F1EA",
- "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d"
+ "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
},
{
"name": "ke",
"unicode": "1F1F0-1F1EA",
- "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d"
+ "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
},
{
"name": "flag_kg",
"unicode": "1F1F0-1F1EC",
- "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e"
+ "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
},
{
"name": "kg",
"unicode": "1F1F0-1F1EC",
- "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e"
+ "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
},
{
"name": "flag_kh",
"unicode": "1F1F0-1F1ED",
- "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c"
+ "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
},
{
"name": "kh",
"unicode": "1F1F0-1F1ED",
- "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c"
+ "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
},
{
"name": "flag_ki",
"unicode": "1F1F0-1F1EE",
- "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638"
+ "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
},
{
"name": "ki",
"unicode": "1F1F0-1F1EE",
- "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638"
+ "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
},
{
"name": "flag_km",
"unicode": "1F1F0-1F1F2",
- "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478"
+ "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
},
{
"name": "km",
"unicode": "1F1F0-1F1F2",
- "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478"
+ "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
},
{
"name": "flag_kn",
"unicode": "1F1F0-1F1F3",
- "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1"
+ "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
},
{
"name": "kn",
"unicode": "1F1F0-1F1F3",
- "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1"
+ "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
},
{
"name": "flag_kp",
"unicode": "1F1F0-1F1F5",
- "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b"
+ "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
},
{
"name": "kp",
"unicode": "1F1F0-1F1F5",
- "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b"
+ "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
},
{
"name": "flag_kr",
"unicode": "1F1F0-1F1F7",
- "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5"
+ "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
},
{
"name": "kr",
"unicode": "1F1F0-1F1F7",
- "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5"
+ "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
},
{
"name": "flag_kw",
"unicode": "1F1F0-1F1FC",
- "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37"
+ "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
},
{
"name": "kw",
"unicode": "1F1F0-1F1FC",
- "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37"
+ "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
},
{
"name": "flag_ky",
"unicode": "1F1F0-1F1FE",
- "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8"
+ "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
},
{
"name": "ky",
"unicode": "1F1F0-1F1FE",
- "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8"
+ "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
},
{
"name": "flag_kz",
"unicode": "1F1F0-1F1FF",
- "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50"
+ "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
},
{
"name": "kz",
"unicode": "1F1F0-1F1FF",
- "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50"
+ "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
},
{
"name": "flag_la",
"unicode": "1F1F1-1F1E6",
- "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb"
+ "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
},
{
"name": "la",
"unicode": "1F1F1-1F1E6",
- "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb"
+ "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
},
{
"name": "flag_lb",
"unicode": "1F1F1-1F1E7",
- "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54"
+ "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
},
{
"name": "lb",
"unicode": "1F1F1-1F1E7",
- "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54"
+ "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
},
{
"name": "flag_lc",
"unicode": "1F1F1-1F1E8",
- "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28"
+ "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
},
{
"name": "lc",
"unicode": "1F1F1-1F1E8",
- "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28"
+ "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
},
{
"name": "flag_li",
"unicode": "1F1F1-1F1EE",
- "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d"
+ "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
},
{
"name": "li",
"unicode": "1F1F1-1F1EE",
- "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d"
+ "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
},
{
"name": "flag_lk",
"unicode": "1F1F1-1F1F0",
- "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693"
+ "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
},
{
"name": "lk",
"unicode": "1F1F1-1F1F0",
- "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693"
+ "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
},
{
"name": "flag_lr",
"unicode": "1F1F1-1F1F7",
- "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc"
+ "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
},
{
"name": "lr",
"unicode": "1F1F1-1F1F7",
- "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc"
+ "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
},
{
"name": "flag_ls",
"unicode": "1F1F1-1F1F8",
- "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89"
+ "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
},
{
"name": "ls",
"unicode": "1F1F1-1F1F8",
- "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89"
+ "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
},
{
"name": "flag_lt",
"unicode": "1F1F1-1F1F9",
- "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f"
+ "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
},
{
"name": "lt",
"unicode": "1F1F1-1F1F9",
- "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f"
+ "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
},
{
"name": "flag_lu",
"unicode": "1F1F1-1F1FA",
- "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c"
+ "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
},
{
"name": "lu",
"unicode": "1F1F1-1F1FA",
- "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c"
+ "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
},
{
"name": "flag_lv",
"unicode": "1F1F1-1F1FB",
- "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47"
+ "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
},
{
"name": "lv",
"unicode": "1F1F1-1F1FB",
- "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47"
+ "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
},
{
"name": "flag_ly",
"unicode": "1F1F1-1F1FE",
- "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68"
+ "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
},
{
"name": "ly",
"unicode": "1F1F1-1F1FE",
- "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68"
+ "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
},
{
"name": "flag_ma",
"unicode": "1F1F2-1F1E6",
- "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16"
+ "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
},
{
"name": "ma",
"unicode": "1F1F2-1F1E6",
- "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16"
+ "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
},
{
"name": "flag_mc",
"unicode": "1F1F2-1F1E8",
- "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf"
+ "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
},
{
"name": "mc",
"unicode": "1F1F2-1F1E8",
- "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf"
+ "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
},
{
"name": "flag_md",
"unicode": "1F1F2-1F1E9",
- "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0"
+ "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
},
{
"name": "md",
"unicode": "1F1F2-1F1E9",
- "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0"
+ "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
},
{
"name": "flag_me",
"unicode": "1F1F2-1F1EA",
- "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff"
+ "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
},
{
"name": "me",
"unicode": "1F1F2-1F1EA",
- "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff"
+ "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
},
{
"name": "flag_mf",
"unicode": "1F1F2-1F1EB",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "mf",
"unicode": "1F1F2-1F1EB",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "flag_mg",
"unicode": "1F1F2-1F1EC",
- "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c"
+ "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
},
{
"name": "mg",
"unicode": "1F1F2-1F1EC",
- "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c"
+ "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
},
{
"name": "flag_mh",
"unicode": "1F1F2-1F1ED",
- "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b"
+ "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
},
{
"name": "mh",
"unicode": "1F1F2-1F1ED",
- "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b"
+ "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
},
{
"name": "flag_mk",
"unicode": "1F1F2-1F1F0",
- "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502"
+ "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
},
{
"name": "mk",
"unicode": "1F1F2-1F1F0",
- "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502"
+ "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
},
{
"name": "flag_ml",
"unicode": "1F1F2-1F1F1",
- "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50"
+ "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
},
{
"name": "ml",
"unicode": "1F1F2-1F1F1",
- "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50"
+ "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
},
{
"name": "flag_mm",
"unicode": "1F1F2-1F1F2",
- "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470"
+ "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
},
{
"name": "mm",
"unicode": "1F1F2-1F1F2",
- "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470"
+ "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
},
{
"name": "flag_mn",
"unicode": "1F1F2-1F1F3",
- "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273"
+ "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
},
{
"name": "mn",
"unicode": "1F1F2-1F1F3",
- "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273"
+ "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
},
{
"name": "flag_mo",
"unicode": "1F1F2-1F1F4",
- "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729"
+ "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
},
{
"name": "mo",
"unicode": "1F1F2-1F1F4",
- "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729"
+ "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
},
{
"name": "flag_mp",
"unicode": "1F1F2-1F1F5",
- "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e"
+ "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
},
{
"name": "mp",
"unicode": "1F1F2-1F1F5",
- "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e"
+ "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
},
{
"name": "flag_mq",
"unicode": "1F1F2-1F1F6",
- "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e"
+ "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
},
{
"name": "mq",
"unicode": "1F1F2-1F1F6",
- "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e"
+ "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
},
{
"name": "flag_mr",
"unicode": "1F1F2-1F1F7",
- "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015"
+ "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
},
{
"name": "mr",
"unicode": "1F1F2-1F1F7",
- "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015"
+ "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
},
{
"name": "flag_ms",
"unicode": "1F1F2-1F1F8",
- "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0"
+ "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
},
{
"name": "ms",
"unicode": "1F1F2-1F1F8",
- "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0"
+ "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
},
{
"name": "flag_mt",
"unicode": "1F1F2-1F1F9",
- "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a"
+ "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
},
{
"name": "mt",
"unicode": "1F1F2-1F1F9",
- "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a"
+ "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
},
{
"name": "flag_mu",
"unicode": "1F1F2-1F1FA",
- "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb"
+ "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
},
{
"name": "mu",
"unicode": "1F1F2-1F1FA",
- "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb"
+ "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
},
{
"name": "flag_mv",
"unicode": "1F1F2-1F1FB",
- "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c"
+ "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
},
{
"name": "mv",
"unicode": "1F1F2-1F1FB",
- "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c"
+ "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
},
{
"name": "flag_mw",
"unicode": "1F1F2-1F1FC",
- "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede"
+ "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
},
{
"name": "mw",
"unicode": "1F1F2-1F1FC",
- "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede"
+ "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
},
{
"name": "flag_mx",
"unicode": "1F1F2-1F1FD",
- "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4"
+ "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
},
{
"name": "mx",
"unicode": "1F1F2-1F1FD",
- "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4"
+ "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
},
{
"name": "flag_my",
"unicode": "1F1F2-1F1FE",
- "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf"
+ "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
},
{
"name": "my",
"unicode": "1F1F2-1F1FE",
- "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf"
+ "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
},
{
"name": "flag_mz",
"unicode": "1F1F2-1F1FF",
- "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9"
+ "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
},
{
"name": "mz",
"unicode": "1F1F2-1F1FF",
- "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9"
+ "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
},
{
"name": "flag_na",
"unicode": "1F1F3-1F1E6",
- "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca"
+ "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
},
{
"name": "na",
"unicode": "1F1F3-1F1E6",
- "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca"
+ "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
},
{
"name": "flag_nc",
"unicode": "1F1F3-1F1E8",
- "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb"
+ "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
},
{
"name": "nc",
"unicode": "1F1F3-1F1E8",
- "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb"
+ "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
},
{
"name": "flag_ne",
"unicode": "1F1F3-1F1EA",
- "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713"
+ "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
},
{
"name": "ne",
"unicode": "1F1F3-1F1EA",
- "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713"
+ "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
},
{
"name": "flag_nf",
"unicode": "1F1F3-1F1EB",
- "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42"
+ "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
},
{
"name": "nf",
"unicode": "1F1F3-1F1EB",
- "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42"
+ "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
},
{
"name": "flag_ng",
"unicode": "1F1F3-1F1EC",
- "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101"
+ "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
},
{
"name": "nigeria",
"unicode": "1F1F3-1F1EC",
- "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101"
+ "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
},
{
"name": "flag_ni",
"unicode": "1F1F3-1F1EE",
- "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894"
+ "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
},
{
"name": "ni",
"unicode": "1F1F3-1F1EE",
- "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894"
+ "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
},
{
"name": "flag_nl",
"unicode": "1F1F3-1F1F1",
- "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910"
+ "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
},
{
"name": "nl",
"unicode": "1F1F3-1F1F1",
- "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910"
+ "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
},
{
"name": "flag_no",
"unicode": "1F1F3-1F1F4",
- "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302"
+ "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
},
{
"name": "no",
"unicode": "1F1F3-1F1F4",
- "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302"
+ "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
},
{
"name": "flag_np",
"unicode": "1F1F3-1F1F5",
- "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957"
+ "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
},
{
"name": "np",
"unicode": "1F1F3-1F1F5",
- "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957"
+ "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
},
{
"name": "flag_nr",
"unicode": "1F1F3-1F1F7",
- "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc"
+ "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
},
{
"name": "nr",
"unicode": "1F1F3-1F1F7",
- "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc"
+ "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
},
{
"name": "flag_nu",
"unicode": "1F1F3-1F1FA",
- "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce"
+ "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
},
{
"name": "nu",
"unicode": "1F1F3-1F1FA",
- "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce"
+ "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
},
{
"name": "flag_nz",
"unicode": "1F1F3-1F1FF",
- "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4"
+ "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
},
{
"name": "nz",
"unicode": "1F1F3-1F1FF",
- "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4"
+ "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
},
{
"name": "flag_om",
"unicode": "1F1F4-1F1F2",
- "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f"
+ "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
},
{
"name": "om",
"unicode": "1F1F4-1F1F2",
- "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f"
+ "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
},
{
"name": "flag_pa",
"unicode": "1F1F5-1F1E6",
- "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458"
+ "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
},
{
"name": "pa",
"unicode": "1F1F5-1F1E6",
- "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458"
+ "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
},
{
"name": "flag_pe",
"unicode": "1F1F5-1F1EA",
- "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00"
+ "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
},
{
"name": "pe",
"unicode": "1F1F5-1F1EA",
- "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00"
+ "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
},
{
"name": "flag_pf",
"unicode": "1F1F5-1F1EB",
- "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa"
+ "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
},
{
"name": "pf",
"unicode": "1F1F5-1F1EB",
- "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa"
+ "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
},
{
"name": "flag_pg",
"unicode": "1F1F5-1F1EC",
- "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044"
+ "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
},
{
"name": "pg",
"unicode": "1F1F5-1F1EC",
- "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044"
+ "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
},
{
"name": "flag_ph",
"unicode": "1F1F5-1F1ED",
- "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5"
+ "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
},
{
"name": "ph",
"unicode": "1F1F5-1F1ED",
- "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5"
+ "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
},
{
"name": "flag_pk",
"unicode": "1F1F5-1F1F0",
- "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646"
+ "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
},
{
"name": "pk",
"unicode": "1F1F5-1F1F0",
- "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646"
+ "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
},
{
"name": "flag_pl",
"unicode": "1F1F5-1F1F1",
- "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617"
+ "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
},
{
"name": "pl",
"unicode": "1F1F5-1F1F1",
- "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617"
+ "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
},
{
"name": "flag_pm",
"unicode": "1F1F5-1F1F2",
- "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717"
+ "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
},
{
"name": "pm",
"unicode": "1F1F5-1F1F2",
- "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717"
+ "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
},
{
"name": "flag_pn",
"unicode": "1F1F5-1F1F3",
- "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3"
+ "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
},
{
"name": "pn",
"unicode": "1F1F5-1F1F3",
- "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3"
+ "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
},
{
"name": "flag_pr",
"unicode": "1F1F5-1F1F7",
- "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308"
+ "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
},
{
"name": "pr",
"unicode": "1F1F5-1F1F7",
- "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308"
+ "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
},
{
"name": "flag_ps",
"unicode": "1F1F5-1F1F8",
- "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13"
+ "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
},
{
"name": "ps",
"unicode": "1F1F5-1F1F8",
- "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13"
+ "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
},
{
"name": "flag_pt",
"unicode": "1F1F5-1F1F9",
- "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395"
+ "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
},
{
"name": "pt",
"unicode": "1F1F5-1F1F9",
- "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395"
+ "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
},
{
"name": "flag_pw",
"unicode": "1F1F5-1F1FC",
- "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe"
+ "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
},
{
"name": "pw",
"unicode": "1F1F5-1F1FC",
- "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe"
+ "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
},
{
"name": "flag_py",
"unicode": "1F1F5-1F1FE",
- "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b"
+ "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
},
{
"name": "py",
"unicode": "1F1F5-1F1FE",
- "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b"
+ "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
},
{
"name": "flag_qa",
"unicode": "1F1F6-1F1E6",
- "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2"
+ "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
},
{
"name": "qa",
"unicode": "1F1F6-1F1E6",
- "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2"
+ "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
},
{
"name": "flag_re",
"unicode": "1F1F7-1F1EA",
- "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a"
+ "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
},
{
"name": "re",
"unicode": "1F1F7-1F1EA",
- "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a"
+ "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
},
{
"name": "flag_ro",
"unicode": "1F1F7-1F1F4",
- "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e"
+ "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
},
{
"name": "ro",
"unicode": "1F1F7-1F1F4",
- "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e"
+ "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
},
{
"name": "flag_rs",
"unicode": "1F1F7-1F1F8",
- "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8"
+ "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
},
{
"name": "rs",
"unicode": "1F1F7-1F1F8",
- "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8"
+ "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
},
{
"name": "flag_ru",
"unicode": "1F1F7-1F1FA",
- "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646"
+ "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
},
{
"name": "ru",
"unicode": "1F1F7-1F1FA",
- "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646"
+ "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
},
{
"name": "flag_rw",
"unicode": "1F1F7-1F1FC",
- "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518"
+ "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
},
{
"name": "rw",
"unicode": "1F1F7-1F1FC",
- "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518"
+ "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
},
{
"name": "flag_sa",
"unicode": "1F1F8-1F1E6",
- "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0"
+ "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
},
{
"name": "saudiarabia",
"unicode": "1F1F8-1F1E6",
- "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0"
+ "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
},
{
"name": "saudi",
"unicode": "1F1F8-1F1E6",
- "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0"
+ "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
},
{
"name": "flag_sb",
"unicode": "1F1F8-1F1E7",
- "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241"
+ "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
},
{
"name": "sb",
"unicode": "1F1F8-1F1E7",
- "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241"
+ "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
},
{
"name": "flag_sc",
"unicode": "1F1F8-1F1E8",
- "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb"
+ "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
},
{
"name": "sc",
"unicode": "1F1F8-1F1E8",
- "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb"
+ "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
},
{
"name": "flag_sd",
"unicode": "1F1F8-1F1E9",
- "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c"
+ "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
},
{
"name": "sd",
"unicode": "1F1F8-1F1E9",
- "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c"
+ "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
},
{
"name": "flag_se",
"unicode": "1F1F8-1F1EA",
- "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d"
+ "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
},
{
"name": "se",
"unicode": "1F1F8-1F1EA",
- "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d"
+ "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
},
{
"name": "flag_sg",
"unicode": "1F1F8-1F1EC",
- "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660"
+ "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
},
{
"name": "sg",
"unicode": "1F1F8-1F1EC",
- "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660"
+ "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
},
{
"name": "flag_sh",
"unicode": "1F1F8-1F1ED",
- "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6"
+ "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
},
{
"name": "sh",
"unicode": "1F1F8-1F1ED",
- "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6"
+ "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
},
{
"name": "flag_si",
"unicode": "1F1F8-1F1EE",
- "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e"
+ "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
},
{
"name": "si",
"unicode": "1F1F8-1F1EE",
- "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e"
+ "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
},
{
"name": "flag_sj",
"unicode": "1F1F8-1F1EF",
- "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78"
+ "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
{
"name": "sj",
"unicode": "1F1F8-1F1EF",
- "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78"
+ "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
{
"name": "flag_sk",
"unicode": "1F1F8-1F1F0",
- "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a"
+ "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
},
{
"name": "sk",
"unicode": "1F1F8-1F1F0",
- "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a"
+ "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
},
{
"name": "flag_sl",
"unicode": "1F1F8-1F1F1",
- "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e"
+ "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
},
{
"name": "sl",
"unicode": "1F1F8-1F1F1",
- "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e"
+ "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
},
{
"name": "flag_sm",
"unicode": "1F1F8-1F1F2",
- "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3"
+ "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
},
{
"name": "sm",
"unicode": "1F1F8-1F1F2",
- "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3"
+ "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
},
{
"name": "flag_sn",
"unicode": "1F1F8-1F1F3",
- "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d"
+ "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
},
{
"name": "sn",
"unicode": "1F1F8-1F1F3",
- "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d"
+ "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
},
{
"name": "flag_so",
"unicode": "1F1F8-1F1F4",
- "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048"
+ "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
},
{
"name": "so",
"unicode": "1F1F8-1F1F4",
- "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048"
+ "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
},
{
"name": "flag_sr",
"unicode": "1F1F8-1F1F7",
- "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874"
+ "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
},
{
"name": "sr",
"unicode": "1F1F8-1F1F7",
- "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874"
+ "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
},
{
"name": "flag_ss",
"unicode": "1F1F8-1F1F8",
- "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7"
+ "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
},
{
"name": "ss",
"unicode": "1F1F8-1F1F8",
- "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7"
+ "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
},
{
"name": "flag_st",
"unicode": "1F1F8-1F1F9",
- "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d"
+ "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
},
{
"name": "st",
"unicode": "1F1F8-1F1F9",
- "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d"
+ "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
},
{
"name": "flag_sv",
"unicode": "1F1F8-1F1FB",
- "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8"
+ "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
},
{
"name": "sv",
"unicode": "1F1F8-1F1FB",
- "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8"
+ "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
},
{
"name": "flag_sx",
"unicode": "1F1F8-1F1FD",
- "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5"
+ "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
},
{
"name": "sx",
"unicode": "1F1F8-1F1FD",
- "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5"
+ "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
},
{
"name": "flag_sy",
"unicode": "1F1F8-1F1FE",
- "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69"
+ "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
},
{
"name": "sy",
"unicode": "1F1F8-1F1FE",
- "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69"
+ "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
},
{
"name": "flag_sz",
"unicode": "1F1F8-1F1FF",
- "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050"
+ "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
},
{
"name": "sz",
"unicode": "1F1F8-1F1FF",
- "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050"
+ "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
},
{
"name": "flag_ta",
"unicode": "1F1F9-1F1E6",
- "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959"
+ "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
},
{
"name": "ta",
"unicode": "1F1F9-1F1E6",
- "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959"
+ "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
},
{
"name": "flag_tc",
"unicode": "1F1F9-1F1E8",
- "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322"
+ "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
},
{
"name": "tc",
"unicode": "1F1F9-1F1E8",
- "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322"
+ "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
},
{
"name": "flag_td",
"unicode": "1F1F9-1F1E9",
- "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6"
+ "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
},
{
"name": "td",
"unicode": "1F1F9-1F1E9",
- "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6"
+ "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
},
{
"name": "flag_tf",
"unicode": "1F1F9-1F1EB",
- "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334"
+ "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
},
{
"name": "tf",
"unicode": "1F1F9-1F1EB",
- "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334"
+ "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
},
{
"name": "flag_tg",
"unicode": "1F1F9-1F1EC",
- "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc"
+ "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
},
{
"name": "tg",
"unicode": "1F1F9-1F1EC",
- "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc"
+ "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
},
{
"name": "flag_th",
"unicode": "1F1F9-1F1ED",
- "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d"
+ "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
},
{
"name": "th",
"unicode": "1F1F9-1F1ED",
- "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d"
+ "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
},
{
"name": "flag_tj",
"unicode": "1F1F9-1F1EF",
- "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398"
+ "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
},
{
"name": "tj",
"unicode": "1F1F9-1F1EF",
- "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398"
+ "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
},
{
"name": "flag_tk",
"unicode": "1F1F9-1F1F0",
- "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2"
+ "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
},
{
"name": "tk",
"unicode": "1F1F9-1F1F0",
- "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2"
+ "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
},
{
"name": "flag_tl",
"unicode": "1F1F9-1F1F1",
- "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7"
+ "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
},
{
"name": "tl",
"unicode": "1F1F9-1F1F1",
- "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7"
+ "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
},
{
"name": "flag_tm",
"unicode": "1F1F9-1F1F2",
- "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f"
+ "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
},
{
"name": "turkmenistan",
"unicode": "1F1F9-1F1F2",
- "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f"
+ "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
},
{
"name": "flag_tn",
"unicode": "1F1F9-1F1F3",
- "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2"
+ "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
},
{
"name": "tn",
"unicode": "1F1F9-1F1F3",
- "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2"
+ "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
},
{
"name": "flag_to",
"unicode": "1F1F9-1F1F4",
- "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300"
+ "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
},
{
"name": "to",
"unicode": "1F1F9-1F1F4",
- "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300"
+ "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
},
{
"name": "flag_tr",
"unicode": "1F1F9-1F1F7",
- "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d"
+ "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
},
{
"name": "tr",
"unicode": "1F1F9-1F1F7",
- "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d"
+ "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
},
{
"name": "flag_tt",
"unicode": "1F1F9-1F1F9",
- "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed"
+ "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
},
{
"name": "tt",
"unicode": "1F1F9-1F1F9",
- "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed"
+ "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
},
{
"name": "flag_tv",
"unicode": "1F1F9-1F1FB",
- "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c"
+ "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
},
{
"name": "tuvalu",
"unicode": "1F1F9-1F1FB",
- "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c"
+ "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
},
{
"name": "flag_tw",
"unicode": "1F1F9-1F1FC",
- "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108"
+ "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
},
{
"name": "tw",
"unicode": "1F1F9-1F1FC",
- "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108"
+ "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
},
{
"name": "flag_tz",
"unicode": "1F1F9-1F1FF",
- "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b"
+ "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
},
{
"name": "tz",
"unicode": "1F1F9-1F1FF",
- "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b"
+ "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
},
{
"name": "flag_ua",
"unicode": "1F1FA-1F1E6",
- "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c"
+ "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
},
{
"name": "ua",
"unicode": "1F1FA-1F1E6",
- "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c"
+ "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
},
{
"name": "flag_ug",
"unicode": "1F1FA-1F1EC",
- "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e"
+ "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
},
{
"name": "ug",
"unicode": "1F1FA-1F1EC",
- "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e"
+ "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
},
{
"name": "flag_um",
"unicode": "1F1FA-1F1F2",
- "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b"
+ "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
},
{
"name": "um",
"unicode": "1F1FA-1F1F2",
- "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b"
+ "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
},
{
"name": "flag_us",
"unicode": "1F1FA-1F1F8",
- "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea"
+ "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
},
{
"name": "us",
"unicode": "1F1FA-1F1F8",
- "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea"
+ "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
},
{
"name": "flag_uy",
"unicode": "1F1FA-1F1FE",
- "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb"
+ "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
},
{
"name": "uy",
"unicode": "1F1FA-1F1FE",
- "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb"
+ "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
},
{
"name": "flag_uz",
"unicode": "1F1FA-1F1FF",
- "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c"
+ "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
},
{
"name": "uz",
"unicode": "1F1FA-1F1FF",
- "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c"
+ "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
},
{
"name": "flag_va",
"unicode": "1F1FB-1F1E6",
- "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7"
+ "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
},
{
"name": "va",
"unicode": "1F1FB-1F1E6",
- "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7"
+ "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
},
{
"name": "flag_vc",
"unicode": "1F1FB-1F1E8",
- "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421"
+ "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
},
{
"name": "vc",
"unicode": "1F1FB-1F1E8",
- "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421"
+ "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
},
{
"name": "flag_ve",
"unicode": "1F1FB-1F1EA",
- "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8"
+ "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
},
{
"name": "ve",
"unicode": "1F1FB-1F1EA",
- "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8"
+ "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
},
{
"name": "flag_vg",
"unicode": "1F1FB-1F1EC",
- "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49"
+ "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
},
{
"name": "vg",
"unicode": "1F1FB-1F1EC",
- "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49"
+ "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
},
{
"name": "flag_vi",
"unicode": "1F1FB-1F1EE",
- "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29"
+ "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
},
{
"name": "vi",
"unicode": "1F1FB-1F1EE",
- "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29"
+ "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
},
{
"name": "flag_vn",
"unicode": "1F1FB-1F1F3",
- "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219"
+ "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
},
{
"name": "vn",
"unicode": "1F1FB-1F1F3",
- "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219"
+ "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
},
{
"name": "flag_vu",
"unicode": "1F1FB-1F1FA",
- "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563"
+ "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
},
{
"name": "vu",
"unicode": "1F1FB-1F1FA",
- "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563"
+ "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
},
{
"name": "flag_wf",
"unicode": "1F1FC-1F1EB",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "wf",
"unicode": "1F1FC-1F1EB",
- "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
{
"name": "flag_white",
"unicode": "1F3F3",
- "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559"
+ "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
},
{
"name": "waving_white_flag",
"unicode": "1F3F3",
- "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559"
+ "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
},
{
"name": "flag_ws",
"unicode": "1F1FC-1F1F8",
- "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164"
+ "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
},
{
"name": "ws",
"unicode": "1F1FC-1F1F8",
- "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164"
+ "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
},
{
"name": "flag_xk",
"unicode": "1F1FD-1F1F0",
- "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e"
+ "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
},
{
"name": "xk",
"unicode": "1F1FD-1F1F0",
- "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e"
+ "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
},
{
"name": "flag_ye",
"unicode": "1F1FE-1F1EA",
- "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078"
+ "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
},
{
"name": "ye",
"unicode": "1F1FE-1F1EA",
- "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078"
+ "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
},
{
"name": "flag_yt",
"unicode": "1F1FE-1F1F9",
- "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e"
+ "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
},
{
"name": "yt",
"unicode": "1F1FE-1F1F9",
- "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e"
+ "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
},
{
"name": "flag_za",
"unicode": "1F1FF-1F1E6",
- "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872"
+ "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
},
{
"name": "za",
"unicode": "1F1FF-1F1E6",
- "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872"
+ "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
},
{
"name": "flag_zm",
"unicode": "1F1FF-1F1F2",
- "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011"
+ "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
},
{
"name": "zm",
"unicode": "1F1FF-1F1F2",
- "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011"
+ "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
},
{
"name": "flag_zw",
"unicode": "1F1FF-1F1FC",
- "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181"
+ "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
},
{
"name": "zw",
"unicode": "1F1FF-1F1FC",
- "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181"
+ "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
},
{
"name": "flags",
"unicode": "1F38F",
- "digest": "c3f4a66786e524a5562919afcba9486113091ed205f1342e91d2f6439845ad61"
+ "digest": "f860aa4df587cf140c3e9735bbd101e9fd5a1bfcea42e420d85ac0a9877fa21d"
},
{
"name": "flashlight",
"unicode": "1F526",
- "digest": "5f641b8fd1c7f1dcd43ec3b1ef78d14ef9929d723789c5567aca8b95d3d39803"
+ "digest": "e929bbe76e0fd2dc5bd6476858a0bbc717fd21467710435d35d80efb38033d73"
},
{
"name": "fleur-de-lis",
"unicode": "269C",
- "digest": "d6ddeeea355ed55103b7fc65ac1ee0dbaa79d01e0d136b265363a6b92284c073"
+ "digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3"
},
{
"name": "flip_phone",
@@ -5322,7 +5322,7 @@
{
"name": "floppy_disk",
"unicode": "1F4BE",
- "digest": "e987961ca516032a90942ef6c398836f2da68a5981714bd172acfe7b0e369d0a"
+ "digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0"
},
{
"name": "floppy_white",
@@ -5337,22 +5337,22 @@
{
"name": "flower_playing_cards",
"unicode": "1F3B4",
- "digest": "451f361050b96ba9ed8dc5b64c8a90c1316fd9b83fb818152881a54e100eea6c"
+ "digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869"
},
{
"name": "flushed",
"unicode": "1F633",
- "digest": "39cf51f9dec2a910c66ecd39a7bd616fea09d67e81801e57e84f03ed1e917750"
+ "digest": "e759d46bab92af5494d78b6c712c06568759afe397e7828ca0a0de1e3eab0165"
},
{
"name": "fog",
"unicode": "1F32B",
- "digest": "da6fdb9b682ed9a3368adcd7531f1a29e22755a620e3cca163fc3f33a6a78107"
+ "digest": "0cbd4733961d30fe0f40f95dd1f37254aebbef26f82dd18ad2000e799eb2898e"
},
{
"name": "foggy",
"unicode": "1F301",
- "digest": "b599f3178db289c6e30017f3f0a9d30b00a75417057c7a10c0c9eedac78edbf1"
+ "digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca"
},
{
"name": "folder",
@@ -5372,52 +5372,52 @@
{
"name": "football",
"unicode": "1F3C8",
- "digest": "834fe5f431d6aa8ef1186aa79e71f813393535d273483b6af4cc4bdb8380e5b4"
+ "digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c"
},
{
"name": "footprints",
"unicode": "1F463",
- "digest": "60dc938f6769ea21b05b5afcc481d3ddacf1f565e04f33310b271d5422e7ceb9"
+ "digest": "85bbf2bc0ae8e6259d83a06f513600095d7fcfc44372670f5b2405d380b78811"
},
{
"name": "fork_and_knife",
"unicode": "1F374",
- "digest": "7e07c9dc555d172fa2eaa41cefd8d46d9624be0137aff196dd003a8a82610ec3"
+ "digest": "f228accd36ddccb4ec636207c19d7185191ec79723b780a1bd5c3d00a4b1ef3b"
},
{
"name": "fork_knife_plate",
"unicode": "1F37D",
- "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247"
+ "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
},
{
"name": "fork_and_knife_with_plate",
"unicode": "1F37D",
- "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247"
+ "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
},
{
"name": "fountain",
"unicode": "26F2",
- "digest": "0acdca5e8f6d745a8d582d96012ec8fc55b9f5447e657ebfd998a4e332d99322"
+ "digest": "87043f9256e1d4615159307fcfd21bf6ae2aba0bada7de2bd50d7d6f2ab82395"
},
{
"name": "four",
"unicode": "0034-20E3",
- "digest": "36bd4ea6e2ae689835a79f8e60466eccd62fce7e91e84ed768cffd87dac628dd"
+ "digest": "c2c82a966bbb599aae557d930a4fc42604f2081aa45528872f5caf4942ee79d9"
},
{
"name": "four_leaf_clover",
"unicode": "1F340",
- "digest": "12ee2343df25bbd9077fdc12314c1edb51c0cdb556af7e22590e8a578ef57f17"
+ "digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8"
},
{
"name": "frame_photo",
"unicode": "1F5BC",
- "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349"
+ "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
},
{
"name": "frame_with_picture",
"unicode": "1F5BC",
- "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349"
+ "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
},
{
"name": "frame_tiles",
@@ -5442,122 +5442,122 @@
{
"name": "free",
"unicode": "1F193",
- "digest": "c1d9172a656717f78d941303c5da8790c6cd9827838d8f7dc3719afb53bcab80"
+ "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa"
},
{
"name": "fried_shrimp",
"unicode": "1F364",
- "digest": "c0c19e95f2c38f6cf870920bf3c2d4d69c36ea6e7dc9a5c45c3e8b285269d40a"
+ "digest": "0792bdc4484852de970c8f43bc3a1a339dc0e48090ec77d6de97cbfcdd17f9e1"
},
{
"name": "fries",
"unicode": "1F35F",
- "digest": "0f546534684de29d319cbcbab4162acb321c4f8f3202fe17d69e1894ab7c8195"
+ "digest": "47915aea67251d358d91a0e4dc3dcc347155336007d6b931a192be72a743b4e9"
},
{
"name": "frog",
"unicode": "1F438",
- "digest": "6a417757fa6ee39e7a277cbd53c690ff88af0b1d76728d56f9bc645cb628aeb7"
+ "digest": "d024b2ce771df64040534fb0906737d18b562bc3578dee62c2f25ec03c7caffd"
},
{
"name": "frowning",
"unicode": "1F626",
- "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5"
+ "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
},
{
"name": "anguished",
"unicode": "1F626",
- "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5"
+ "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
},
{
"name": "frowning2",
"unicode": "2639",
- "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da"
+ "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
},
{
"name": "white_frowning_face",
"unicode": "2639",
- "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da"
+ "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
},
{
"name": "fuelpump",
"unicode": "26FD",
- "digest": "9cbb2646c93b255bd3de87dc01aa1193ab96e39a3013975d250472ab8aae61d6"
+ "digest": "105e736469f19911b8bab4ab6d29f949ded4b061b54e3dd763726577d6453095"
},
{
"name": "full_moon",
"unicode": "1F315",
- "digest": "0b4f08ef2089397ead034b444a60e6e9810073454581b52a46b2369e3b9cd5f9"
+ "digest": "aaa87f4676a5aaa29c1b721a3b582e89db6c1f35a25c52e4b480bd193ef39c43"
},
{
"name": "full_moon_with_face",
"unicode": "1F31D",
- "digest": "a371cb9e1f28a7db739dd058234642a2e333dff4b6df9882df85a6d984e4b5e8"
+ "digest": "05c4b9c339fcdf81ae67027641522baa99c370d87873ff4c8133b8349e627e33"
},
{
"name": "game_die",
"unicode": "1F3B2",
- "digest": "6584909a4348c350c04417421b63eace1245087f7d239051b30a0cd37fe929f9"
+ "digest": "00d19ce8e21dba2cdfeb18709fa8741f3af9d6207f81d5657b68e05e64f105a8"
},
{
"name": "gear",
"unicode": "2699",
- "digest": "b0ff5fd007daa366a9eecb7422dbeb8a973e123a04267b88fef96c7453238294"
+ "digest": "c5ba354c0f7a36dce95477091984e352ecc59af8c9f26a94ad8e296dc042b9de"
},
{
"name": "gem",
"unicode": "1F48E",
- "digest": "d75d854f35975e4e291c3b9fcaf8437467f6d7eb27b29e2d7c0f0038fc666fe2"
+ "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
},
{
"name": "gemini",
"unicode": "264A",
- "digest": "392abe62872736a0bf92979a8c25a814985d0ff0a08dc7ab2a5c058aeda7e685"
+ "digest": "278239c598d490a110f1f3f52fc3b85259be8e76034b38228ef3f68d7ddd8cdd"
},
{
"name": "ghost",
"unicode": "1F47B",
- "digest": "f084b14483476e2d07563840f8c33b46da9c17f791da07fde3acffeb77342947"
+ "digest": "80d528fcf8ef9198631527547e43a608a4332a799f9e5550b8318dec67c9c4d2"
},
{
"name": "gift",
"unicode": "1F381",
- "digest": "c9a2ae6ea05c02e78e9567dcbd971701a2f869eb46c62d85cef23d0834388d8c"
+ "digest": "4061a84a59f0300473299678c43e533341eb965db09597fffc6e221fd7b77376"
},
{
"name": "gift_heart",
"unicode": "1F49D",
- "digest": "e0c5aacf1ce89117d86b148f10a02dc18fe0cd22a75fbf6f0f88f2fad3ca80fe"
+ "digest": "5420199b515b9b32c964a3c19d87e07461639e3068a939dae26c6436335c0cee"
},
{
"name": "girl",
"unicode": "1F467",
- "digest": "0758cbc4cbc7d72d6df8f66fc3a6b2b283c6634b053e59d61c6cac44cf8bffda"
+ "digest": "8d2d0b72a91e6e44921b71030ffc4c89c0f50f1364787784afe1e7e568cf1bc6"
},
{
"name": "girl_tone1",
"unicode": "1F467-1F3FB",
- "digest": "7afdece55cb64e8056e2202de8c17b66ddb616f224ac374ec9a160d06b3138cc"
+ "digest": "bda12a6b38994a578ee65166bbdd93ea04df4101697b52ed236de8d687df09de"
},
{
"name": "girl_tone2",
"unicode": "1F467-1F3FC",
- "digest": "c160aa65fee70ad52930d01246ac9f282ff6abf1d93c5cc5b299fc257ee81db1"
+ "digest": "de7a0925c30b7181a289f71b1a849c1b7751ee8c104e8f2029bd9c2fe3f91c64"
},
{
"name": "girl_tone3",
"unicode": "1F467-1F3FD",
- "digest": "b8a5687cd637855a41b8c7dc686f0e69fda379875408cd269f1b330a805c72f4"
+ "digest": "e41272816db0e642d003dce7cb262e1593a592251f46729f7830f4515149e1f2"
},
{
"name": "girl_tone4",
"unicode": "1F467-1F3FE",
- "digest": "a9cf743936b733634f323790a1abe3a410601b6841484baebea484b392f4e98e"
+ "digest": "8d6a4513ecbf08408c0ecc5336767777a2216f7a19437faf9e51f65101822469"
},
{
"name": "girl_tone5",
"unicode": "1F467-1F3FF",
- "digest": "c902170e67b81eee35eeefb6a5c62c6109cb423dcae88d4e036ddd50b240c072"
+ "digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d"
},
{
"name": "girls_symbol",
@@ -5567,172 +5567,172 @@
{
"name": "globe_with_meridians",
"unicode": "1F310",
- "digest": "945646de3d8f057760fe374494a253d9a6aa8a132309154b0a5bdbffb5b20c3f"
+ "digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75"
},
{
"name": "goat",
"unicode": "1F410",
- "digest": "f99cbc6755d119cb5c1dce08cabd20871f98d009bb773da4a146dae60476a235"
+ "digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8"
},
{
"name": "golf",
"unicode": "26F3",
- "digest": "74a7876d185f8ff6a6533e4db2e1eb787119b2f8d8b07c36d99ec3163fb48485"
+ "digest": "eed79364754eec97855e3c7b584f347ae139d9ddb4eb7fb66c00867610b8f1c1"
},
{
"name": "golfer",
"unicode": "1F3CC",
- "digest": "6458295a5e4a6e4323c32a7f1f7182fb2d3918083839efc380d995860ce360b1"
+ "digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344"
},
{
"name": "grapes",
"unicode": "1F347",
- "digest": "7f6873d65180ab476f49d207ac2d1f7dbaf6c8b0b561d50b64325e192cf97a86"
+ "digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50"
},
{
"name": "green_apple",
"unicode": "1F34F",
- "digest": "effc3fe60f2ab704a034c794bfccfa023b41332f8f16ca44cc8ea41698f03873"
+ "digest": "457490e9b2b20894f50768262d63f1021717079da104d4847076b3fa779e9a21"
},
{
"name": "green_book",
"unicode": "1F4D7",
- "digest": "6652c4d2ccfa4a287a5d45007bd06cadc16d34b0a1ca4b6b13b46f976c8d8319"
+ "digest": "370f635b200efe5e4a9f17da58bd22500e258e61d17795cef375f19c9a45468f"
},
{
"name": "green_heart",
"unicode": "1F49A",
- "digest": "f4bcb660a1d3cf3692238359d8b9de9a725a9af81f166253e487d61b8ccf9d86"
+ "digest": "f71e30416d9019873f2ed38ef375c48386424ff60b5a07b89b15dc9e0a3970f9"
},
{
"name": "grey_exclamation",
"unicode": "2755",
- "digest": "ac8cdab7496d133e7bc9475f2fdb0cf59b3ccba20f2f156c8b693e72b5948078"
+ "digest": "2fa1d356e12c17cc4025e43afb6c3070385f677102a35223302fda46c47a9b03"
},
{
"name": "grey_question",
"unicode": "2754",
- "digest": "c173e1b2a16ab62b0abd7a58deb7a6df709b072d30d001627b92d0123a3a3e4a"
+ "digest": "e1035bcbf0f66d238ef478ba451f5cf2c51627fbf101ed03bad3b2bf38db8aa2"
},
{
"name": "grimacing",
"unicode": "1F62C",
- "digest": "8c54b73f5d2c1c6347e2c0ab01616519e0fb34490daa9c36664d442c6851c57e"
+ "digest": "2cedad13b8b2a1d4385ca6fa88a251eb7757a4c65dd6d362267864a01247846b"
},
{
"name": "grin",
"unicode": "1F601",
- "digest": "916eabdabd8b7ca698e638bbbd14affff97464ec11a3b59c0cb96cd7705600d8"
+ "digest": "634b2f37e32e57ed6edc7f371993a92e34137dd21ba393de5227cfbbe2422815"
},
{
"name": "grinning",
"unicode": "1F600",
- "digest": "3d8665c03f272ca3063e96145989926355a7ac315ed1a032d30fcefa6f0c3923"
+ "digest": "cef76aa41771db9fd1d6bd9b4233c22c1fb1931494af54cab29e6347ed9b678d"
},
{
"name": "guardsman",
"unicode": "1F482",
- "digest": "ebbd29fa138005232d64fca4a8ec015d097fa14e6ded57b35ac257b4570b3c36"
+ "digest": "17bc7fad6b8c8dbd015bb709380d129f8b8e1e971062d15e6ab0b2e63e500564"
},
{
"name": "guardsman_tone1",
"unicode": "1F482-1F3FB",
- "digest": "b6082c8fee5dbc3ce2540f3939d5e344b5366c9f07827345facaba438e7017ff"
+ "digest": "c531ecb101bdf9ce1db18e1567882e6db927410237100b0a2492a1401860246e"
},
{
"name": "guardsman_tone2",
"unicode": "1F482-1F3FC",
- "digest": "2b813afe1c2bbdaf9a47493393a0e6c400a16e453ed25a9a9c0035197927b56e"
+ "digest": "602168c5204af0f1de8b4aa5863b192ef20c19d263999377aa5eb60f98311732"
},
{
"name": "guardsman_tone3",
"unicode": "1F482-1F3FD",
- "digest": "49b2fa1ad0bc50a5ef6d73fb140aa1876506b9ebb9d45782ccb8dbb6818f8dde"
+ "digest": "d0a85de46dd02c7bd6cb14bff0f22d2db9083d4b171a8806c83363b49f3dd9ef"
},
{
"name": "guardsman_tone4",
"unicode": "1F482-1F3FE",
- "digest": "a584e1e3a8ad7be4871a6bdb7996d4f649abeaa77eb5d1cae998058d8b23ca0f"
+ "digest": "1c9d4d72b6b50bdac8271613b6d2a38340ec2067bc344e8ee2a3c863fd5c23a1"
},
{
"name": "guardsman_tone5",
"unicode": "1F482-1F3FF",
- "digest": "e853b67ee13fda99e98f47083529ca80c404df1b19352c78b9c69850eb8f2c76"
+ "digest": "9899a796d01842e495d716fbe737a16d85724f7d3e23f50807ec2bc70f057318"
},
{
"name": "guitar",
"unicode": "1F3B8",
- "digest": "8c041b961649cc5917f56f2fb543f9a5280724647ed2fc67bc94a05eff9da805"
+ "digest": "a1027ceae4dd3ea270740587c9d373329e5677e375c9e00af6ae3275e0b67500"
},
{
"name": "gun",
"unicode": "1F52B",
- "digest": "d7f5aa657cc0ba04d878511820632b89c305a9b4d6c4a4b90ff691dad9906607"
+ "digest": "fc12b577df2283e7b336f23774f9cfe5b79f1d26ddd28a64a560519b28d94ca5"
},
{
"name": "haircut",
"unicode": "1F487",
- "digest": "369dbab1b138c31d3eca04c950fdab4ec9f085272268c241f100d44e7b0f229e"
+ "digest": "b243a04f5ca889accd45e7abe095ac5caa92274ed95103f5966a36b415fff412"
},
{
"name": "haircut_tone1",
"unicode": "1F487-1F3FB",
- "digest": "c56f32d7c1d8a92d22429133f87f31a159818939cfdc570cb48b6d243cc58cf2"
+ "digest": "a58d0cff1427b80dfd7a9ea5267b4a181e9faaac6a51a0165db522f668b4cf91"
},
{
"name": "haircut_tone2",
"unicode": "1F487-1F3FC",
- "digest": "e916e040ffb8e869e930d1256343af2ad2bbaa683f01a11564d0777019944bec"
+ "digest": "675083ff40001405f8de99268477d50dd8594ff6ca40ddfd442dd42ad76e8216"
},
{
"name": "haircut_tone3",
"unicode": "1F487-1F3FD",
- "digest": "f07cdfbea964ac42a9a050f832107ef0f2fa8115b27689f93d1be954de07b7c1"
+ "digest": "70d7581e49c315a3771dd61a3713229886db32aaaeb3af078a69cc042f809150"
},
{
"name": "haircut_tone4",
"unicode": "1F487-1F3FE",
- "digest": "32ec7f5e999f7c43676768c8320ffaa346c713d340a94b948b1f564b345a2d11"
+ "digest": "ec5e3e909eb3bc375ef9cc0fe0e0f90b33f44f273ada91ccf415bbc43b8ffbfc"
},
{
"name": "haircut_tone5",
"unicode": "1F487-1F3FF",
- "digest": "5aad997d09e7975700927906d41a10bae774356ccddbe5197980bde670272262"
+ "digest": "7c89739ee458546a808fded7f96d9354c47a76883ebb262d5f5abeafd021260e"
},
{
"name": "hamburger",
"unicode": "1F354",
- "digest": "24ebae9a69cf283ab198499cb38d0cdcd82bac74c8e8d1e769ad78eb320a4294"
+ "digest": "48204235238bd89d3a69f319f65135102f3d6b181eec241d4d86b302bbffa9bf"
},
{
"name": "hammer",
"unicode": "1F528",
- "digest": "a43a66b0efdc4cd2c84fd0ccc2cb8e9ede1f89c5d62eefa6ae521d3aed9d81b3"
+ "digest": "d0e7830539d935fcd82820c4e0c1d724f0756dfc83a51171fe0f4b36b69fac42"
},
{
"name": "hammer_pick",
"unicode": "2692",
- "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2"
+ "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
},
{
"name": "hammer_and_pick",
"unicode": "2692",
- "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2"
+ "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
},
{
"name": "hamster",
"unicode": "1F439",
- "digest": "f47da088ff5792532a382b6e3a47d2dd7c5e6fc19abd5ff6c5ba3ce420b4192e"
+ "digest": "a7e7582e8b1bccd5b7df27ccb05e353a3f0e39bdeb40877732706b9d74a70de1"
},
{
"name": "hand_splayed",
"unicode": "1F590",
- "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197"
+ "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
},
{
"name": "raised_hand_with_fingers_splayed",
"unicode": "1F590",
- "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197"
+ "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
},
{
"name": "hand_splayed_reverse",
@@ -5747,52 +5747,52 @@
{
"name": "hand_splayed_tone1",
"unicode": "1F590-1F3FB",
- "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe"
+ "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
},
{
"name": "raised_hand_with_fingers_splayed_tone1",
"unicode": "1F590-1F3FB",
- "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe"
+ "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
},
{
"name": "hand_splayed_tone2",
"unicode": "1F590-1F3FC",
- "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145"
+ "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
},
{
"name": "raised_hand_with_fingers_splayed_tone2",
"unicode": "1F590-1F3FC",
- "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145"
+ "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
},
{
"name": "hand_splayed_tone3",
"unicode": "1F590-1F3FD",
- "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e"
+ "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
},
{
"name": "raised_hand_with_fingers_splayed_tone3",
"unicode": "1F590-1F3FD",
- "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e"
+ "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
},
{
"name": "hand_splayed_tone4",
"unicode": "1F590-1F3FE",
- "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3"
+ "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
},
{
"name": "raised_hand_with_fingers_splayed_tone4",
"unicode": "1F590-1F3FE",
- "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3"
+ "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
},
{
"name": "hand_splayed_tone5",
"unicode": "1F590-1F3FF",
- "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8"
+ "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
},
{
"name": "raised_hand_with_fingers_splayed_tone5",
"unicode": "1F590-1F3FF",
- "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8"
+ "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
},
{
"name": "hand_victory",
@@ -5807,7 +5807,7 @@
{
"name": "handbag",
"unicode": "1F45C",
- "digest": "f1e2822c67f659b52c76821dd9db001332215a8566fc1846c89b6019c9758038"
+ "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45"
},
{
"name": "hard_disk",
@@ -5817,67 +5817,67 @@
{
"name": "hash",
"unicode": "0023-20E3",
- "digest": "5bd5c7180485fa71accdec5378bdc196ce0602f594f91e4eadc1e7514d5d0f90"
+ "digest": "01c8b577953010bff0c20f797c2c96ab5d98d4e6ac179c4895a78f34ea904655"
},
{
"name": "hatched_chick",
"unicode": "1F425",
- "digest": "7995c3eb503a8b9662694eba80a9b551216473a31928091e35cd6ebc21cee083"
+ "digest": "006571b9e9e839ec9fcb1a911b935c8ca71eb8bcdce9775bee6a2a4c7c927277"
},
{
"name": "hatching_chick",
"unicode": "1F423",
- "digest": "22905b42fa65dbc9aad8940d2db13691cacc62014f54e0960978ee0002178e1b"
+ "digest": "fd7f69fa186407f80de59dec5116e318325a5743ee0e8bba1db541f1e57e7f74"
},
{
"name": "head_bandage",
"unicode": "1F915",
- "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08"
+ "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
},
{
"name": "face_with_head_bandage",
"unicode": "1F915",
- "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08"
+ "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
},
{
"name": "headphones",
"unicode": "1F3A7",
- "digest": "219da138032c01c97a94f02b211049418191a3beb3d159804b9033f5916fd3c8"
+ "digest": "34f9d5598158d5d6f978a5ea5c5aa9948bb2990625565a3afad7710f864fbe2f"
},
{
"name": "hear_no_evil",
"unicode": "1F649",
- "digest": "8120060238eaca645809dd113862a144f10395afcb3837ab60c0f04009b49a2f"
+ "digest": "53b030b6d6f4ed1a734fa7d48b46f42eb1b2b01653202c1838b742082f08c4bf"
},
{
"name": "heart",
"unicode": "2764",
- "digest": "a646a25a36f431cadc7e56afd1a4d1b7cbae5292a25d7783bd31462d0d3d719b"
+ "digest": "92be652ec3e50c6e7393440b5d52b88a367f98a28dffe12660095ed3253aa6c0"
},
{
"name": "heart_decoration",
"unicode": "1F49F",
- "digest": "a83989669347c98cb74065d4f0befedbc37f82c91214e773245cb6810ab359b4"
+ "digest": "6ec5bbf3aa75c6f43eb3dc05e9204366936e8b6b4219310bacdc2fc45f51e245"
},
{
"name": "heart_exclamation",
"unicode": "2763",
- "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2"
+ "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
},
{
"name": "heavy_heart_exclamation_mark_ornament",
"unicode": "2763",
- "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2"
+ "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
},
{
"name": "heart_eyes",
"unicode": "1F60D",
- "digest": "335ea73efca4824e623a5a51ccdb494c8b1f5f10b4139b39b250a2a771876b0d"
+ "digest": "0eff616517a6252ec89d47d9b4ad85589bcf2bdc7f490578934350acb84b2fcc"
},
{
"name": "heart_eyes_cat",
"unicode": "1F63B",
- "digest": "9346b85afb80f7b498cc255426ea15a287f81d8fb3c26dab61337635f439d3ce"
+ "digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6"
},
{
"name": "heart_tip",
@@ -5892,257 +5892,257 @@
{
"name": "heartbeat",
"unicode": "1F493",
- "digest": "cd6921ce55c155873220a09416d695c4bcca1556007066d6d185e93d6561e825"
+ "digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe"
},
{
"name": "heartpulse",
"unicode": "1F497",
- "digest": "f869357b9e678d9671ec38c569fc88efec48006c159b69297277cee795dc4dc9"
+ "digest": "281d8aebfea37db5b7fe82d9115be167006881fe29ab64a5b09ac92ac27a2309"
},
{
"name": "hearts",
"unicode": "2665",
- "digest": "17dc9b2941561f58ca0f04d0754b1eff3490b63b17241580b3d4aa4638fa85e8"
+ "digest": "271429d12c40be921897005b7bdd08f9518960af1e1e6f56bb0060f1f183651e"
},
{
"name": "heavy_check_mark",
"unicode": "2714",
- "digest": "b5fa24f6e0f1dcbd6278e9125154522f2efd79e6dd0836ccb792a1f3aeeff2b2"
+ "digest": "e347728e1290eb9e7b0742d628e2fd124fc049e0774f8a6ddf8e5286e7318718"
},
{
"name": "heavy_division_sign",
"unicode": "2797",
- "digest": "59a6983d788f347c64eecb3df6f7d3b36779d92df6cc811820993ff9e18d77e1"
+ "digest": "c1e8c40f0788f140b1c5fcb81ed9b5ce1bcfa5988bb8140ed2808e9cb7e0d651"
},
{
"name": "heavy_dollar_sign",
"unicode": "1F4B2",
- "digest": "d2e89c54b3fdeda4d1fd4d29454b69dcf750181110894e6e71a40df99c95bfe8"
+ "digest": "7cdeef38348654b93d566e01a48973281cb404a63d0b75b3bad51032887f3f55"
},
{
"name": "heavy_minus_sign",
"unicode": "2796",
- "digest": "dd5ab3722fe49cfdbc5e1fbab5b342dc960de7b412d4fba59d66e06ce3dc3bcd"
+ "digest": "e5335cc6b22abdce49a6127c34269b65a4a6643ddd3253d9baac425089143e7d"
},
{
"name": "heavy_multiplication_x",
"unicode": "2716",
- "digest": "7d77742f91377785675802f40bd8dde9bd1feeb513735760a58ea9bee8a65d44"
+ "digest": "64bbe9e9716a922e405d2f6d3b6d803863a53fac80ff8cd775899971046cb1ca"
},
{
"name": "heavy_plus_sign",
"unicode": "2795",
- "digest": "9aa9dcdbba120a4b485c21f67589609b789c6e3edf08479ff8268fa0db973ad7"
+ "digest": "d0d8ade2020ceb252205180b85c66e665856e6cb505518d395b9913b0b24b746"
},
{
"name": "helicopter",
"unicode": "1F681",
- "digest": "b259ea8d2bdca36766075894da650b1d3ff4c8602259cd0d30cb8214cd585340"
+ "digest": "4bd6fd13650fbe3a19cfffeffe6c21b1cda74bd6af64c5dc5999185e35444bc3"
},
{
"name": "helmet_with_cross",
"unicode": "26D1",
- "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4"
+ "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
},
{
"name": "helmet_with_white_cross",
"unicode": "26D1",
- "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4"
+ "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
},
{
"name": "herb",
"unicode": "1F33F",
- "digest": "3c452106b1966f643751bf161fa7d1762a33e6fff381b2109bb53b55c4fdd129"
+ "digest": "9fe8ed65515ede59d0926dcf98f14e2498785e1965610aa0dd56eca9b4bedad9"
},
{
"name": "hibiscus",
"unicode": "1F33A",
- "digest": "268963a1f3cdad9050d9ae31c558e010f33812e3b09bbf9088ba876c033d8b2f"
+ "digest": "c442e8eacbd8727bd154bd39692a9a2a03ea2f674b9670ad8361f78a038afe49"
},
{
"name": "high_brightness",
"unicode": "1F506",
- "digest": "d607f6269d95dd16c2a7932e49ac09e44f4c19e0a34f6c0f21ecb945a2316361"
+ "digest": "35ced42426dcfd5214c2c6c577dce84bb708156433945e6b6adaff7ea530cc57"
},
{
"name": "high_heel",
"unicode": "1F460",
- "digest": "5c320d5954bf4f4dacacddd562c1598ab101731077a6656ac5d2bfd41405483e"
+ "digest": "1e7c7aba50eb1d02cf1d9aa372caca741a6005cf47f68dfa75b7310c3cb18f05"
},
{
"name": "hockey",
"unicode": "1F3D2",
- "digest": "008904c1b8db139215492a6d96c09f2c3eeda769f858a9bbae13f8c54d439d0e"
+ "digest": "2d00fb17baa617e799db8e9b1771cc365bb4545c7633df0123e66e1a6e2ed25d"
},
{
"name": "hole",
"unicode": "1F573",
- "digest": "36bbafa5e89b1410ec74919aaf60b09ac3525a421cb5b475b9bb2f20357db8de"
+ "digest": "8b5539f6f24f09d5d68ffd56be5aa2a8a2f753a8dfbf64892fb02c8f2703e920"
},
{
"name": "homes",
"unicode": "1F3D8",
- "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9"
+ "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
},
{
"name": "house_buildings",
"unicode": "1F3D8",
- "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9"
+ "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
},
{
"name": "honey_pot",
"unicode": "1F36F",
- "digest": "94cb1624491076b5cb145e7a309f91a7be3d4c0bed712af6a51d641eb73edee7"
+ "digest": "f6eec8c32fbd1b461446dc6c5d5031c43e6ee9685dc9b1ea1b839114e48c4eee"
},
{
"name": "horse",
"unicode": "1F434",
- "digest": "624ad9dc9ed7af3f6e1a2f9d4ed483702ae64ed5fbcf5e9918af6bfef24e76f9"
+ "digest": "e377649a9549835770a2a721a92570f699255f88efa646029638eb8ec5f10e3d"
},
{
"name": "horse_racing",
"unicode": "1F3C7",
- "digest": "c2702b7225e9839a789dda7c43f0cc86dced2b4d5d3787116106396633362de6"
+ "digest": "3b98e94e9c028ad85b9a750cc61db5ee3ac23cf5ad9243ea3e996b1f772bad54"
},
{
"name": "horse_racing_tone1",
"unicode": "1F3C7-1F3FB",
- "digest": "a7ed284f9d5cd8a4fe4a09cb91c3f99e5db99c7e31c5f525c14de97b06857d92"
+ "digest": "382d8e4502ed34fc1bbf1779ce483bc2e22b83f89c91746c11a5d7aea656d446"
},
{
"name": "horse_racing_tone2",
"unicode": "1F3C7-1F3FC",
- "digest": "20b4d61b21ee6ba860b029f0ad0e38f5ecb6dd2c774f7b7801fba07ed33f96be"
+ "digest": "198df9973b492ea63e5cfc210dd9591750ccce04a6380adc1dc5b4cb0462a8cd"
},
{
"name": "horse_racing_tone3",
"unicode": "1F3C7-1F3FD",
- "digest": "dd65f7bb96ee44507d26e524202d567d2d7679d571245299a2a84f68bd5def4c"
+ "digest": "a67f95fc92c366750ebad3c4db92982893d67a5ed78163c8cc809ac40d2ab9a3"
},
{
"name": "horse_racing_tone4",
"unicode": "1F3C7-1F3FE",
- "digest": "36afaad218a4c820b19c7c9bbbc187119d47b41273d8f48ab14cc3e32dd7c21f"
+ "digest": "986b1706c4a3395b58a8ae3b7609ffdd4424dfefcbf26c88c8085f4f6379734e"
},
{
"name": "horse_racing_tone5",
"unicode": "1F3C7-1F3FF",
- "digest": "2e0efd501a4471428533ce7909972a49ff045369261c27e4abb97ee2aede2f47"
+ "digest": "66656b5e3d0f43f16f983f9db6214b07aac73b143eeff6475782f98aa5b9ba53"
},
{
"name": "hospital",
"unicode": "1F3E5",
- "digest": "df5c774fa36b2601e6960a7b81cdfac71c1d2d71f04dea88068d1c9043e313bb"
+ "digest": "034573e76df444f5b0eb7aff3a4103e4b49a1813869155ab3ae29a6fc0c6c8a2"
},
{
"name": "hot_pepper",
"unicode": "1F336",
- "digest": "62e4dade3c793f6d83530bd1f60f3e3e26c1e10a41786c3a15f5aec0ff2b8e76"
+ "digest": "0b05777d42698196a10db17d04030175b1dfa772d06288f71d666d5f8d3fddbc"
},
{
"name": "hotdog",
"unicode": "1F32D",
- "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b"
+ "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
},
{
"name": "hot_dog",
"unicode": "1F32D",
- "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b"
+ "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
},
{
"name": "hotel",
"unicode": "1F3E8",
- "digest": "428120a35b38a217901e10d704751eb8fdbc9f805e6eccd8aab070f4311b2085"
+ "digest": "2d78e0ad4cfb0caad778c7de49fefd6e8356afe902a43e3f1c40bceb6b0be422"
},
{
"name": "hotsprings",
"unicode": "2668",
- "digest": "df4f946218445f97a6f28c6abe4c1d1dac56ff97a8cd81df59f1b3c320e0092f"
+ "digest": "4c10c3a974b44693e8cbe91365c8b8d7f14f62db234cc516b6e54c08a6bacaed"
},
{
"name": "hourglass",
"unicode": "231B",
- "digest": "07aece9413e6898717b4f0757e073d7a593f3e8044c56855127033b796207ccb"
+ "digest": "f0bae8392aaf6f75a83f5d8914936b8650665b24ba1b232fa546b71545dd9acd"
},
{
"name": "hourglass_flowing_sand",
"unicode": "23F3",
- "digest": "92dbc68e9d16fb9f706236367e1882f0d2b6817b83ca490820a000021f2c6483"
+ "digest": "2d077729f40fc04007a933e97356bd511cbd8be76b8c55962ca3fa0d8b828e23"
},
{
"name": "house",
"unicode": "1F3E0",
- "digest": "a6221fc84a9b0e11ae71bfa1e0020982b55ff8c89a374a6d755dba710b4e058c"
+ "digest": "b4ac25979fbe161ada0d2a75769aa7552d2371d37d78cddba4ffdc7f076d3279"
},
{
"name": "house_abandoned",
"unicode": "1F3DA",
- "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668"
+ "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
},
{
"name": "derelict_house_building",
"unicode": "1F3DA",
- "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668"
+ "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
},
{
"name": "house_with_garden",
"unicode": "1F3E1",
- "digest": "22d0d911da96b7ae3bf6692d3cf3590afbca959fc99c13e7a088f7194f43a35d"
+ "digest": "817463f23ec0a849393ba75c333e822b4d253cd4db998c127e90d1b924f35d20"
},
{
"name": "hugging",
"unicode": "1F917",
- "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240"
+ "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
},
{
"name": "hugging_face",
"unicode": "1F917",
- "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240"
+ "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
},
{
"name": "hushed",
"unicode": "1F62F",
- "digest": "69faa8e0b170ee8cf41977ca4a5154406360ed9699d5c62ecdaa01f50e8e4276"
+ "digest": "22586107f7399eff64538a52929dade152633aa268fc5ec4e6fe1c0e00a7bd89"
},
{
"name": "ice_cream",
"unicode": "1F368",
- "digest": "d48ec98a8789148b96c30f19595201a0f85ed899659d97d1d3596091162909ff"
+ "digest": "d1a8e685f2ecf83dead28733859e369d6ce120a2669cdab97dc4423547d472ac"
},
{
"name": "ice_skate",
"unicode": "26F8",
- "digest": "6fb044d9fbe62605f6728062c35c345ddd3ae4cc51203c925b0e69f1b3ef2dbf"
+ "digest": "41ef65c143bc068868fa64080ffd447d91aa3fe2a39e69ecaa97022820af4dcd"
},
{
"name": "icecream",
"unicode": "1F366",
- "digest": "abd5774157575dd304dc1a393244757853972c863861a654ca29b2d528e48b28"
+ "digest": "22cfe17b80cbd2a0377ee90da45bd40d33533c914b2639d363fbb1f00714e194"
},
{
"name": "id",
"unicode": "1F194",
- "digest": "860ffb36d37d84e2c1cf0ab991b95c1cf73e458bef0e4d85bb0c1e26115cb2d1"
+ "digest": "bcf0922e083821d3be7951893084ea0d72a0110ef0b20d11dfec24dd70633893"
},
{
"name": "ideograph_advantage",
"unicode": "1F250",
- "digest": "37892a5642cd49ef7828646f36f48b5a83dc02437624c05da428579256118030"
+ "digest": "0b6bf59f63fda1afa92d652814a778a056c3f4abdd9cf3f6796068bd71783051"
},
{
"name": "imp",
"unicode": "1F47F",
- "digest": "f8c93d03bd9f1d5ef86738541e11695d6811bf6fef06759eba98321b6d038814"
+ "digest": "52598cf2441988f875ccb4e479637baefc679e3ca64e9a6400e56488b0fde811"
},
{
"name": "inbox_tray",
"unicode": "1F4E5",
- "digest": "066a2d75633eb50329496f6866b5b0645c2e48135a03118f1bf53244f8529043"
+ "digest": "d5d9497022b5318fcfbfdfcd56df9c65dd8f4a4cb5e6283ca260836df57da301"
},
{
"name": "incoming_envelope",
"unicode": "1F4E8",
- "digest": "ef6e5c5aa679d174181dae77113717f26e295778dde1e2c3bdf1d64de8a4af8c"
+ "digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a"
},
{
"name": "info",
@@ -6157,97 +6157,97 @@
{
"name": "information_desk_person",
"unicode": "1F481",
- "digest": "acae6d272e348aee87dd60360f16ac58cea7cb4e1ea962cc1655005c7f4aed27"
+ "digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064"
},
{
"name": "information_desk_person_tone1",
"unicode": "1F481-1F3FB",
- "digest": "709ebb0481ca981d76ece2d4fc68db693ddf18b9c1aaa0b6ac5d3c42e71bf07f"
+ "digest": "6674f2e059eff7cfd7fd6abc800da37c4f1087feb4ff26c9e4e31aa29fdf9921"
},
{
"name": "information_desk_person_tone2",
"unicode": "1F481-1F3FC",
- "digest": "d5bc3563bc721d66b73850db93ac827be3715e7ca6420dc0051396ffe26bef47"
+ "digest": "9983412ecd130b7e9cfb078167016c06fd043b6f9f3c26d21733ca3f059fd109"
},
{
"name": "information_desk_person_tone3",
"unicode": "1F481-1F3FD",
- "digest": "af67fd4ef2fc402bec2d446b2e8ff5e9f636b5a9bbb6639587cdb88bd780d265"
+ "digest": "d8907bf47af5722127afca8fc0da587eab33044a6c60a94890983deb8d6f7a66"
},
{
"name": "information_desk_person_tone4",
"unicode": "1F481-1F3FE",
- "digest": "fd3174d1adfe13e8c0d6b6ae9c3a26ea35bb40f98f0728f91d1798809a74933b"
+ "digest": "3be086d4edfe9ca8e4a364b4e8d09b81b5b594b5eeb9ffdf6370179fb3118658"
},
{
"name": "information_desk_person_tone5",
"unicode": "1F481-1F3FF",
- "digest": "4b773c443830a02de8b4d6471077b5d1387b560b537cabba7cdc667110cbde69"
+ "digest": "2fde4e98dd11c5c29c89cad7cbb7bd2d5077dfad07913b20e01955b2d0dfad40"
},
{
"name": "information_source",
"unicode": "2139",
- "digest": "50cd8bf46d20b7c18d5f00a69fc79452aa32934245ba8d0929e51632d73876bd"
+ "digest": "b6bf3cce86d42c2e3c46470baab4af01e900b8ae337b605c3da07c3eba671269"
},
{
"name": "innocent",
"unicode": "1F607",
- "digest": "a3510fd51c17093ebe2371cfde7611aa44aed2d120a0e5500cfaae0f1d3486a4"
+ "digest": "20f8d856bc3e46f4b1173cea05d4577e1c61f06b2daba46e57db90f4066bb428"
},
{
"name": "interrobang",
"unicode": "2049",
- "digest": "1f843ff672486154f9f3df549bb1b528a5eac8d15264f447649ba57f45ee4d00"
+ "digest": "92a2d5b4c0bd6714e402f6f12fe19774cb41d081b5e9c23c415ce794224d8117"
},
{
"name": "iphone",
"unicode": "1F4F1",
- "digest": "be6f96c02ddae557f700fd20fe7b3f94c9e1c928acb82b2b8b214d231273fece"
+ "digest": "1ebc54215713cd4bf1c1e50770999f2512bb4fea29e37d0bb3a8aa2460ff875d"
},
{
"name": "island",
"unicode": "1F3DD",
- "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa"
+ "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
},
{
"name": "desert_island",
"unicode": "1F3DD",
- "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa"
+ "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
},
{
"name": "izakaya_lantern",
"unicode": "1F3EE",
- "digest": "ddb20f475aa119c3a64a55dff40f7a9dbc3a14f7ffc6cfbac89210c652f10d02"
+ "digest": "fbdc290e666d43d0776a73b955c26df4518692b35e72742e073705fc4ca2ae88"
},
{
"name": "jack_o_lantern",
"unicode": "1F383",
- "digest": "62a701ac472619bcb3859e0d9a61b98c7f5c32150d2d04ca8c3e8fc3bec4dbd5"
+ "digest": "78d666c2e80f64bfb6796f53e5ba4960a83ec36192110e8661031bee2b5e370a"
},
{
"name": "japan",
"unicode": "1F5FE",
- "digest": "2535300fff2b2e4b75fc73c187be6c0ea4bc4753e443db498ea55e268e627ab7"
+ "digest": "e7d9d6ebf9047fdd3c52e074ba259659c6d8e51a6abae3cdb8d6cf6dbf9a93fe"
},
{
"name": "japanese_castle",
"unicode": "1F3EF",
- "digest": "70645aa05599e23a9ac4327e4a2e78bffe7ea06c38ec1935c15ae420619c5c1c"
+ "digest": "938ae132c403330288223b88d28c19a47224d4f254fbc2366ecef73d9633112c"
},
{
"name": "japanese_goblin",
"unicode": "1F47A",
- "digest": "59b6901dc6eedc6509c25b4eef6702bf461ded06c5ff12fe2a02a5b3301577c0"
+ "digest": "63d4bcf58b9d0c29612994432aad2ae35819fdd2890674e60a2f1d51601b742e"
},
{
"name": "japanese_ogre",
"unicode": "1F479",
- "digest": "dab7e68cd4cbf99c13d64792c7104c4f0a846bc63aa12950fa8fab028dca301d"
+ "digest": "434ceedd102e7dcbc07e086811673dd63659ddf8c3ec4d029a3d759a0abfcbdb"
},
{
"name": "jeans",
"unicode": "1F456",
- "digest": "ddd032ac77cdfe49152a0e0a0eaaaea9f183590fb1f493ec30e9e39f679e3914"
+ "digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5"
},
{
"name": "jet_up",
@@ -6262,37 +6262,37 @@
{
"name": "joy",
"unicode": "1F602",
- "digest": "f90cfbcb14f906f8d786b61f022c978f381fc99ca422805f605631314e101805"
+ "digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08"
},
{
"name": "joy_cat",
"unicode": "1F639",
- "digest": "6ca24a94490de66d1ca2cbc080bcd805f54ca295051d8e6588cae3fe6658c80a"
+ "digest": "a65c999604147e5e20170fcb14f80a1ff0a633f991492e1f790b2ad4caec7b7e"
},
{
"name": "joystick",
"unicode": "1F579",
- "digest": "ec172df88ef8e8a5512d6d906c13296875b7057ed0cca79f4ac8cddd9e1de34b"
+ "digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd"
},
{
"name": "kaaba",
"unicode": "1F54B",
- "digest": "30f1a27a148399bbb811586eff795eff858701c42055c23e4d5bef7ae77f5f32"
+ "digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6"
},
{
"name": "key",
"unicode": "1F511",
- "digest": "c68ed648350d3976c8d27a709020c8873ecf553929e66453acff96231684a1a2"
+ "digest": "66719fa77a50a0827c8d47237e2704c03e38186e6fef80627a765473b2294c2e"
},
{
"name": "key2",
"unicode": "1F5DD",
- "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d"
+ "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
},
{
"name": "old_key",
"unicode": "1F5DD",
- "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d"
+ "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
},
{
"name": "keyboard",
@@ -6327,132 +6327,132 @@
{
"name": "keycap_ten",
"unicode": "1F51F",
- "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40"
},
{
"name": "kimono",
"unicode": "1F458",
- "digest": "e92bea044fe013f1993c2229d86e9cca9d43f14aab00564ce6ff559bdc5ce93a"
+ "digest": "637182590e256c8fb74ce4c0565f5180c07f06e3bdebf30138ed3259b209c27f"
},
{
"name": "kiss",
"unicode": "1F48B",
- "digest": "c060eb09af2a0d0f77d307b995c15719b0e59c9162a490b8a553fac9b779c8f0"
+ "digest": "62f9b9ffcb01558cd5bb829344a1d1d399511663ff5235405c1f786c9416a94d"
},
{
"name": "kiss_mm",
"unicode": "1F468-2764-1F48B-1F468",
- "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63"
+ "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
},
{
"name": "couplekiss_mm",
"unicode": "1F468-2764-1F48B-1F468",
- "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63"
+ "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
},
{
"name": "kiss_ww",
"unicode": "1F469-2764-1F48B-1F469",
- "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5"
+ "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
},
{
"name": "couplekiss_ww",
"unicode": "1F469-2764-1F48B-1F469",
- "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5"
+ "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
},
{
"name": "kissing",
"unicode": "1F617",
- "digest": "3142617e8b9488689bd9efc67c0e4cc71a1870df8ffc308f949eedc5c3684051"
+ "digest": "b4a505f9e3d7fbd0ac60111f0e678cf425a5fd1abc65a3e9db59ae4abcfb8e85"
},
{
"name": "kissing_cat",
"unicode": "1F63D",
- "digest": "ed26cee8c438ba41365b55c48457cdad3e8d43bf90db3128ac5b277718b82ed3"
+ "digest": "a00431bf10601db4998e78433279167e52cbd36aed885399482529d5cdab8636"
},
{
"name": "kissing_closed_eyes",
"unicode": "1F61A",
- "digest": "22d3369d21b4c2cb4c0c2cab9551cd848dd4f9adecfa64977d3f1a80fc0c8b53"
+ "digest": "ae474db7daf80fe0b82ae1f2a11672cfcd9f9126e100f6e6d4b8a0d135dce39d"
},
{
"name": "kissing_heart",
"unicode": "1F618",
- "digest": "1f089b07447bdcc1baada6a2a9607d4ef4f2de9a6093fcab47a553a64b9acb76"
+ "digest": "bce372573bd3b347b555c1cd22087e03e650df73c8e0284ab668bf6633251632"
},
{
"name": "kissing_smiling_eyes",
"unicode": "1F619",
- "digest": "e37d282861669adfa3953b9af833acfab7d55e787621d4318d77de7e3529d5c5"
+ "digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f"
},
{
"name": "knife",
"unicode": "1F52A",
- "digest": "3fef068a6ada61630dc868e47d25e0e0550b44bc7cf530afe88ca63dc7ab2a39"
+ "digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df"
},
{
"name": "koala",
"unicode": "1F428",
- "digest": "fe020ab9048f3c2a881474f8b1335db6bfaf37d115ff9b2d264f668d136122dd"
+ "digest": "c58f7e0abae42c2218a85efed0e04151df67187815bebca7f3db6f435e0dab4d"
},
{
"name": "koko",
"unicode": "1F201",
- "digest": "734a5cb296826a598e02be3f4ec22f318633ede2ce274914586256421e2df97b"
+ "digest": "5f45eb49bbf298e1fadedfe6cccc297850fcaaa4535e4cc911d48d979af55807"
},
{
"name": "label",
"unicode": "1F3F7",
- "digest": "9fe8195c3efab4d905b1cfcba0ae58cda12496030b0908de8076ff5e6777742e"
+ "digest": "9550ed50cedbc56eb1bd22a8a0809d837048a33d6e2e6e7d65c50d95fa05a85d"
},
{
"name": "large_blue_circle",
"unicode": "1F535",
- "digest": "ba4d0f84a9c2be9a65b25c8cfa78f30d4856d021b1853154dd1d2fd0c5bcfb6a"
+ "digest": "0df3fb3b09a6269459a3d9a1fe78db572190a948680844cfe758f53b6a482ff4"
},
{
"name": "large_blue_diamond",
"unicode": "1F537",
- "digest": "d5aa5e315126859c10c83507be6b9e11cbf423f7a27145de089468cff9b94a94"
+ "digest": "7f646b4e9de2788ed09e45f72cb512c269dda4989029b39bf9a2556659321651"
},
{
"name": "large_orange_diamond",
"unicode": "1F536",
- "digest": "108600badd0ef267842325c0fbf326cb3504306332c64f6f5694de2b54c9438a"
+ "digest": "80ae005ef9d79190c777f00de0993f8b3cb783f7051d76e971640c8c0827c338"
},
{
"name": "last_quarter_moon",
"unicode": "1F317",
- "digest": "68315b85bc1cb17bb82629bd1a6024a5124f3641b9878a732a8aad016c587546"
+ "digest": "3d1f276607c685d50f4b70d00a57750a57ad9ad84256dafd2dc8eef8c72300c3"
},
{
"name": "last_quarter_moon_with_face",
"unicode": "1F31C",
- "digest": "146a419109b7f662bf87cf9de299e47d025a8758c8970b7dabf3483e1956b559"
+ "digest": "d516825ba52dc67f5a01433fb9df2aa77742d38efde4225983ebc4882cbdfe5d"
},
{
"name": "laughing",
"unicode": "1F606",
- "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5"
+ "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
},
{
"name": "satisfied",
"unicode": "1F606",
- "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5"
+ "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
},
{
"name": "leaves",
"unicode": "1F343",
- "digest": "f65e2db125564eb04fc427a49fff175d6e2dae847bd12314d5e6a131610d5ccd"
+ "digest": "56a7a0e767a6f214d340d1b5989efd99fec52c6aa306ec5c3328e32234a1631b"
},
{
"name": "ledger",
"unicode": "1F4D2",
- "digest": "62df1772cec10c035ae0646e6cca4ba7d75b10636a520d091c5b42c2dc36b742"
+ "digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4"
},
{
"name": "left_luggage",
"unicode": "1F6C5",
- "digest": "62292758715115e55ab6239805b7f99b7b35bdfa8d40da07fe391424f1f083d8"
+ "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf"
},
{
"name": "left_receiver",
@@ -6467,107 +6467,107 @@
{
"name": "left_right_arrow",
"unicode": "2194",
- "digest": "28a6945972451b1f4dadec5c55310b8868ffd9f3b0a07803287bc4e07a56e7d4"
+ "digest": "560fcf1b794eb0d5269c73b3f8da57540cbb8a6f1a9af7a9d10b202252247e34"
},
{
"name": "leftwards_arrow_with_hook",
"unicode": "21A9",
- "digest": "d672afc39fd50f78d7370be243173fe76ba50292f0c401305b562898939a8b7f"
+ "digest": "504714c5559b1bd35aa469be83069a923d1a25f364cac08c10df0195749e7b26"
},
{
"name": "lemon",
"unicode": "1F34B",
- "digest": "e0e293a8b8c1b3c87534f5e05cf006671eb3c6d52b4d17d40f2e23bce215a8be"
+ "digest": "ccca25bb6ac47770dba3aaf75144128f9a73299061969b25a35ad1733dcde5fe"
},
{
"name": "leo",
"unicode": "264C",
- "digest": "b0fd4e5f4637de530b62323521c6edcd80312d67ea4043eedd959acb6763474a"
+ "digest": "f2ed930e279699962f189e0cac519cc29d339b3e82debfdc90c5b0935a7543bb"
},
{
"name": "leopard",
"unicode": "1F406",
- "digest": "ede891be8484a17e6277431c64ec1bfd6b742544a41947ebc85005bc2d558bb1"
+ "digest": "d4a8964b6f2cdf6ddf074d0f1f2f65783a1a43eb4af426905fad0e60899939c7"
},
{
"name": "level_slider",
"unicode": "1F39A",
- "digest": "49777cf160d9130d723e3bfef765c3de54033e6b059000fb0e22fb559b5ed190"
+ "digest": "48842324f54d971ebf548a89a82ac7f29e235702081c91b477b1a92d427290e7"
},
{
"name": "levitate",
"unicode": "1F574",
- "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044"
+ "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
},
{
"name": "man_in_business_suit_levitating",
"unicode": "1F574",
- "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044"
+ "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
},
{
"name": "libra",
"unicode": "264E",
- "digest": "ec8e2e7a735abc9f2bddb115fc0e09f4bdc7a164679e2b57d127f58eee1155c2"
+ "digest": "e330ba05bb449db074bc23d1514246ca5e249110f44ddb5804e5510eef6deac1"
},
{
"name": "lifter",
"unicode": "1F3CB",
- "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3"
+ "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
},
{
"name": "weight_lifter",
"unicode": "1F3CB",
- "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3"
+ "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
},
{
"name": "lifter_tone1",
"unicode": "1F3CB-1F3FB",
- "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01"
+ "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
},
{
"name": "weight_lifter_tone1",
"unicode": "1F3CB-1F3FB",
- "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01"
+ "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
},
{
"name": "lifter_tone2",
"unicode": "1F3CB-1F3FC",
- "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f"
+ "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
},
{
"name": "weight_lifter_tone2",
"unicode": "1F3CB-1F3FC",
- "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f"
+ "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
},
{
"name": "lifter_tone3",
"unicode": "1F3CB-1F3FD",
- "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d"
+ "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
},
{
"name": "weight_lifter_tone3",
"unicode": "1F3CB-1F3FD",
- "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d"
+ "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
},
{
"name": "lifter_tone4",
"unicode": "1F3CB-1F3FE",
- "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6"
+ "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
},
{
"name": "weight_lifter_tone4",
"unicode": "1F3CB-1F3FE",
- "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6"
+ "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
},
{
"name": "lifter_tone5",
"unicode": "1F3CB-1F3FF",
- "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5"
+ "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
},
{
"name": "weight_lifter_tone5",
"unicode": "1F3CB-1F3FF",
- "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5"
+ "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
},
{
"name": "light_check_mark",
@@ -6582,27 +6582,27 @@
{
"name": "light_rail",
"unicode": "1F688",
- "digest": "7c2be55456f1332e849ff6699a26dda2e1641c280f45c9ec88dedf6d9b7b7fe2"
+ "digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1"
},
{
"name": "link",
"unicode": "1F517",
- "digest": "cc4873f8a612dd721dddcd507a4430b4fb6c4abc15a8848456f0ffd97811b163"
+ "digest": "7bf567aabd1fc38b3d70422f9db3a13b50950cf6207e70962c9938827c196ccb"
},
{
"name": "lion_face",
"unicode": "1F981",
- "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9"
+ "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
},
{
"name": "lion",
"unicode": "1F981",
- "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9"
+ "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
},
{
"name": "lips",
"unicode": "1F444",
- "digest": "e3bc20f9e210fa1711271234fe61bf1c9ddf36dd6ffc5b832c6c3a769a1e59a8"
+ "digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e"
},
{
"name": "lips2",
@@ -6612,477 +6612,477 @@
{
"name": "lipstick",
"unicode": "1F484",
- "digest": "335b912e163020df3d6d9f0a19a55d6547bd59b471c5a3e374c2968e49911ccc"
+ "digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1"
},
{
"name": "lock",
"unicode": "1F512",
- "digest": "c20eacfb8ccd9bb85919a837c0d4650ee608edb48c85bff46945f613e95d7038"
+ "digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83"
},
{
"name": "lock_with_ink_pen",
"unicode": "1F50F",
- "digest": "5cab25cea08e22d9c3f5de16de6d0ab658ca15cc93d7830f29b0f3e9348ec45f"
+ "digest": "7b5e959b26cf7296c7b230fc2be9feb9e38391c5001951a019d16b169a71aba9"
},
{
"name": "lollipop",
"unicode": "1F36D",
- "digest": "33d2334a00bf0e15869ccc75fadc36f27f89abf0525bb71f859aad9e1dc4ad66"
+ "digest": "17b6a0df47ec758a2f9c087b46a6902cee344d39407ef4c321e408505cbb72ca"
},
{
"name": "loop",
"unicode": "27BF",
- "digest": "fa1174ddc44e317d0796e07868c7ac8ac9c9274fbc8a6c3d0ec78d543c3c6bf0"
+ "digest": "9f20ecc34b3c871789ba7d0712aa31e7a74b6c1558ac8bea385bc40590056726"
},
{
"name": "loud_sound",
"unicode": "1F50A",
- "digest": "fb70229e13b690ffc1031d2e631123f8c908035a15218c297c1c4a3ff3624aa0"
+ "digest": "64b12db9ddd8adf74a9fc2bd83c7979ea865113347f7ce8666e9ccf5019e715f"
},
{
"name": "loudspeaker",
"unicode": "1F4E2",
- "digest": "e2d6cf9ec6412ee62f3128a1afd8c63ec74755c4833f01a4f99722407fe154d6"
+ "digest": "1e1f35d16dd2898ebaa6f2b2868203df6e09c8a70df069c92d6d1b5cb2ac0976"
},
{
"name": "love_hotel",
"unicode": "1F3E9",
- "digest": "184670ebc4045043a7b18d576da3255d216551da522a11cde7df34524e9c7d50"
+ "digest": "ff8966a50fd47a216855488eb09a367d231fea21f49e7e5325191d32fb494473"
},
{
"name": "love_letter",
"unicode": "1F48C",
- "digest": "9a4c52e2622fc7d364995ebc93ca530d972134621d117b72053a659dffc90ffc"
+ "digest": "037261c8ca4d72f7205e51664591696da2ae7ceb19f1c1c9f6123da5a5979d29"
},
{
"name": "low_brightness",
"unicode": "1F505",
- "digest": "c177b7fa9fdbef959cc47e7d16becd71117470b767a81ed6d15f80f464776c02"
+ "digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4"
},
{
"name": "m",
"unicode": "24C2",
- "digest": "2eaf011e74d69613923dad424daaec4c13b592388dbcc5757b645bc058eedecb"
+ "digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4"
},
{
"name": "mag",
"unicode": "1F50D",
- "digest": "029427bd73d2c79fffc5194ded01f6011952ec0124b7634c6230e0afa7ad7c95"
+ "digest": "a6e31a2efa7d9427aaa30b45d9f4181ee55c44be08aea2df165a86e0e6d9eaa1"
},
{
"name": "mag_right",
"unicode": "1F50E",
- "digest": "f99de50bb59ec3bf1d4ccb8584ca09d4a7ceb5bf9f600ea8d3f84930efbf01b8"
+ "digest": "c7d8ceeb05db261e5eaab31dc4da432d0d5592a2ed71e526c5a542daa230bbaf"
},
{
"name": "mahjong",
"unicode": "1F004",
- "digest": "da5d1fa980c38e092d414516161ca26046aa65ace3261999ea750f72e676ac6e"
+ "digest": "755d69f988434ce1c17531a8b7ac92ead6f5607c2635a22f10e0ad70f09fc3e6"
},
{
"name": "mailbox",
"unicode": "1F4EB",
- "digest": "14217df8f39a95fc0a0c527f97db1ca8564764034e921614decc5be705629352"
+ "digest": "2069091be90a530a43ef29d5ec7688c351bf4d5b08d63a0d20d72b67d639ec62"
},
{
"name": "mailbox_closed",
"unicode": "1F4EA",
- "digest": "e0c7beb205ec548a66d8afc7f103b64c6c79c08417ab550f19c36cc6d1a62bc4"
+ "digest": "d88d65bfebb8216535fd055c69f319564b2cf0b0901820f8312f581864557ed4"
},
{
"name": "mailbox_with_mail",
"unicode": "1F4EC",
- "digest": "6d381c0c4181be628d9409df1d85f8a9438c21ef5b92d82ef8ae1ff0079236de"
+ "digest": "69e966b4659128991a70c6a2dd4d647551bedb91bdf5ce688958686bbec56381"
},
{
"name": "mailbox_with_no_mail",
"unicode": "1F4ED",
- "digest": "74843d5ea9e03b48323f2252bdd000585f549b7fffe1fe181a25c38b99b5e23d"
+ "digest": "9e92d8ee88f660ce56da61077c80ec26c5d8f54ebd2306c4cfa16f6c1b981f83"
},
{
"name": "man",
"unicode": "1F468",
- "digest": "0275935258b4c832c3fcb06531d3e6972e2c3d46bab2973004750a9f00bd4cb6"
+ "digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7"
},
{
"name": "man_tone1",
"unicode": "1F468-1F3FB",
- "digest": "1f6603d040f4a025f49d384170dd16b8da169663fc3282af1dc8710d9c1a7adf"
+ "digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504"
},
{
"name": "man_tone2",
"unicode": "1F468-1F3FC",
- "digest": "d65bb03071b483946c69c61769d19b29a2af76fa7e43020e55f0bbc046492221"
+ "digest": "7ebc64de40d3ac60fb761be5cf94f53fa10b4f03fb66add46c90f5d98eaf71eb"
},
{
"name": "man_tone3",
"unicode": "1F468-1F3FD",
- "digest": "9af8ede7211b19a7dc0c60db083dd2bdc4897dda4d71e57feadf2e39d847f060"
+ "digest": "77ceef4d3740ed4751acb83dd45b6b754cf625c522c6757309cd4d61202d7149"
},
{
"name": "man_tone4",
"unicode": "1F468-1F3FE",
- "digest": "6555de60976aafeb024db78addb44eab2a412dd7277013f44d06757d03b6a252"
+ "digest": "41e6037c393f61cca61b9a81b27ed14a95d75fe380e3a00153c33a371a836ffd"
},
{
"name": "man_tone5",
"unicode": "1F468-1F3FF",
- "digest": "b58b97a28a6adc1777acc05194cd917c730f90e37441124c384ded12e9a7d2a4"
+ "digest": "a8cebfd39a5b9c79af7cc37f205e1135376056fee287af967c9f55d415572d99"
},
{
"name": "man_with_gua_pi_mao",
"unicode": "1F472",
- "digest": "88663173a6ccbebec5e24883c90d965447e022c6688773273110fe544d5b1607"
+ "digest": "3dae285e900c69986a48db0fa89d4f371a49f38608059cdae52be098030c5ac4"
},
{
"name": "man_with_gua_pi_mao_tone1",
"unicode": "1F472-1F3FB",
- "digest": "3c8bad3923a619f888e14544d357499a26a517e8fbe7a51027117b960c9eb842"
+ "digest": "35404d8e266920c78edd9e7143fb052b42f65242a5698494c4f4365e9183cc67"
},
{
"name": "man_with_gua_pi_mao_tone2",
"unicode": "1F472-1F3FC",
- "digest": "da125a3310fab19c9282497d53e2fc71ad07920ce60a0ef52dcdb31500023f09"
+ "digest": "82d4f968665a93c7543372c8a1eeb0f25d0ea6842d5e518bd91c226c6c3ab8c2"
},
{
"name": "man_with_gua_pi_mao_tone3",
"unicode": "1F472-1F3FD",
- "digest": "1d5842558847367966bf3ea473ff80fe744359bc5d969f4cc06cf2e452ed2fb6"
+ "digest": "f44159f0c672b9b833449382896180e799abf574f5b3c6cd9541caa992fa18ce"
},
{
"name": "man_with_gua_pi_mao_tone4",
"unicode": "1F472-1F3FE",
- "digest": "92be490f3ba602a43e2be8160d8bfd8a0691b2f81fe017b06df10f476a89ffab"
+ "digest": "c79060188f9461ca34eaa225b7682d8c410883609509fb731c992db69bfeeb50"
},
{
"name": "man_with_gua_pi_mao_tone5",
"unicode": "1F472-1F3FF",
- "digest": "669f6b31bc7a8bf50b169d0600f14e00addaeb24144a1bace8b94950372839b0"
+ "digest": "de9e4acdb10f7abddeeabc0b48d91139fc8b544a601c530db811f099991b0d38"
},
{
"name": "man_with_turban",
"unicode": "1F473",
- "digest": "87d30d35ba40ee39c2df8ce19d975ce34a9c54688bafeac7377d7d481e55f1a4"
+ "digest": "db72c944e93983f38d00e3e936ebb5b243c6069f1f1236d46f6a9f1beb8d6634"
},
{
"name": "man_with_turban_tone1",
"unicode": "1F473-1F3FB",
- "digest": "33b8b8154e0691e2ad66177dbf1e0101411fd8b3a16bf4e54c36d4a874f2a275"
+ "digest": "b6d7489c4cd151af09fff48b62c54c336303e14866e6ef38f94cd834b085d09e"
},
{
"name": "man_with_turban_tone2",
"unicode": "1F473-1F3FC",
- "digest": "1a6b83faa8d6e6a7d12a04898a6f22243287330a1faa081d2626b17dfb07174d"
+ "digest": "7854ef973c21847f452d7e78e5c460ea300e12b539ce92c69dabe8f1bf3a4382"
},
{
"name": "man_with_turban_tone3",
"unicode": "1F473-1F3FD",
- "digest": "5d43da5109e688ff8ca0675f33ebbaf930e206f1f01e3ee773f2844663fe572b"
+ "digest": "1dbd9bd78f5263cbadee7d0d5754c14cfbc914f7329e25fbd97d9f5b8ce0737e"
},
{
"name": "man_with_turban_tone4",
"unicode": "1F473-1F3FE",
- "digest": "bfaf7293c5ea75d0ecdc6fe5afe8f48e7b29b2e0df06ef974d3e1732f5db5dd4"
+ "digest": "4f4804da4a7c98ad4f9db3ae3eaf674c8977c638e73414e33ef1f65098e413a3"
},
{
"name": "man_with_turban_tone5",
"unicode": "1F473-1F3FF",
- "digest": "fba2404dd3d7eab5268519894cc0b386e1b17fdf14a04760c346014aa0e25acd"
+ "digest": "240282aa346ef9b1d0d475ea93a02597697f0f56f086305879b532b0b933210a"
},
{
"name": "mans_shoe",
"unicode": "1F45E",
- "digest": "45dc13ac44c922b4c4b8ecb2e1a870a78e09d53da86843431ab0e9ec96ebcd97"
+ "digest": "f53fe74abd9906cd3e2dd7e7bddbe1feb9f8f7be28b807fabe452f1f60ca1b84"
},
{
"name": "map",
"unicode": "1F5FA",
- "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b"
+ "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
},
{
"name": "world_map",
"unicode": "1F5FA",
- "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b"
+ "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
},
{
"name": "maple_leaf",
"unicode": "1F341",
- "digest": "40c5ee93396301911391cf6e70454b6fa8020fe5c85d3364136bcedb5d052cdb"
+ "digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72"
},
{
"name": "mask",
"unicode": "1F637",
- "digest": "e0301cd27eb8c74c9772ff05b880215fc031ac1ae7f3177cd24ba0acb43b3834"
+ "digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600"
},
{
"name": "massage",
"unicode": "1F486",
- "digest": "856d0fb1144ee91c58dfad74f9a2cababf6bae4b3ceba2a95c03ecd44ae3aa21"
+ "digest": "6ee48b4d8cec0bf31e11d7803ad9fc1f909457c8c00cb320b5671395af3c170c"
},
{
"name": "massage_tone1",
"unicode": "1F486-1F3FB",
- "digest": "fd53b06eb0967303c0914ebb79fd872900ec0f71b2852c7238517e192e5023e1"
+ "digest": "9da162c2f39628156b87db986a6ada59372a9e9a6b3f0488d21c9e65ec3309bb"
},
{
"name": "massage_tone2",
"unicode": "1F486-1F3FC",
- "digest": "7ef57359a339ae1ca4488f9a6195a352e74daf5b67d8e1ae1e91fe866921c40c"
+ "digest": "ac259188549b5b429b8c4929e1da2314859e8857ee49720551467aedfcc96567"
},
{
"name": "massage_tone3",
"unicode": "1F486-1F3FD",
- "digest": "e4fb643b6242bedb395e503ae337a88b2a255b5fda88b4aaa93396f948614a6e"
+ "digest": "cfd9c105b6debc10448f172afcb20d4192899f7ae5aa8af54c834153a5466364"
},
{
"name": "massage_tone4",
"unicode": "1F486-1F3FE",
- "digest": "94f007c2daf9455fa8d2b10cc7ccff7db9bc9daf835ef5c3699be091938db833"
+ "digest": "38ab715c621c58454f3cb09153a96380118cf082568554b6edc5f83fb62e9297"
},
{
"name": "massage_tone5",
"unicode": "1F486-1F3FF",
- "digest": "d18e800b728bf45b500f492062dc81312ca1ad7b1a0277a3d5bc150e4632ea1c"
+ "digest": "32480457734121b0c83e9be6d693ae379c95535f43f963c0c2f0f20434ee12c6"
},
{
"name": "meat_on_bone",
"unicode": "1F356",
- "digest": "674a2a58e174b7681eef3b6c5b39c098ed9374cc610d037166c0092ee5269a97"
+ "digest": "d71a8e0b118d5e6ca60690793ce9649afb78e707fcbd7be890a75564c94434fd"
},
{
"name": "medal",
"unicode": "1F3C5",
- "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6"
+ "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
},
{
"name": "sports_medal",
"unicode": "1F3C5",
- "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6"
+ "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
},
{
"name": "mega",
"unicode": "1F4E3",
- "digest": "540ab4fd5bab041a681749b85e6de598ebcbfc4fbf5c3cdbd9ca1e8256191733"
+ "digest": "4b1def6b5b051c5045514063f0ac006222ad81fbfe56d840e14bb950713e331b"
},
{
"name": "melon",
"unicode": "1F348",
- "digest": "39dd0ecb23e2d3da6cbb7309333fed5d7e2cb38c0afc526ade78520eca11b5f4"
+ "digest": "0cdd663e6f2129808856cdf0746e6571b62aac641f224adb553baf3bb63ba3bd"
},
{
"name": "menorah",
"unicode": "1F54E",
- "digest": "5f81bc2e5a34bf76481d2958fdb0b4e4540c599aa837a6453609a39023885d8c"
+ "digest": "49fca8c3bc00ea69653ee2f8d4e21e561856ba39716c13e9d107db3e805a2997"
},
{
"name": "mens",
"unicode": "1F6B9",
- "digest": "5ed56cff80e8ee7ed581f2a2e365915db5cb29df89e850e0add0b68db4b0c788"
+ "digest": "7d92292586ee12a5d1a557c37da4d14708dc3ce701cf32d3280dcc83d91e5df8"
},
{
"name": "metal",
"unicode": "1F918",
- "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6"
+ "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
},
{
"name": "sign_of_the_horns",
"unicode": "1F918",
- "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6"
+ "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
},
{
"name": "metal_tone1",
"unicode": "1F918-1F3FB",
- "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa"
+ "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
},
{
"name": "sign_of_the_horns_tone1",
"unicode": "1F918-1F3FB",
- "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa"
+ "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
},
{
"name": "metal_tone2",
"unicode": "1F918-1F3FC",
- "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6"
+ "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
},
{
"name": "sign_of_the_horns_tone2",
"unicode": "1F918-1F3FC",
- "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6"
+ "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
},
{
"name": "metal_tone3",
"unicode": "1F918-1F3FD",
- "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0"
+ "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
},
{
"name": "sign_of_the_horns_tone3",
"unicode": "1F918-1F3FD",
- "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0"
+ "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
},
{
"name": "metal_tone4",
"unicode": "1F918-1F3FE",
- "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011"
+ "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
},
{
"name": "sign_of_the_horns_tone4",
"unicode": "1F918-1F3FE",
- "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011"
+ "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
},
{
"name": "metal_tone5",
"unicode": "1F918-1F3FF",
- "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56"
+ "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
},
{
"name": "sign_of_the_horns_tone5",
"unicode": "1F918-1F3FF",
- "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56"
+ "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
},
{
"name": "metro",
"unicode": "1F687",
- "digest": "532378cf385f9a7fafe2f5c8203e675be6d38798871f4c8e2c50498a1529f956"
+ "digest": "b380247b61b5e2ca1b9b70fabff65907b2c3a5191a14b169ae094af94659b9b1"
},
{
"name": "microphone",
"unicode": "1F3A4",
- "digest": "46da2b94e4dc233f640249103f09ec915aaa812cce90afe68fedb6774a27ad4b"
+ "digest": "9ef4fc2e40d5391c4bb2d30f34f59662cff7cbb1b04341c9dac210d0e21b44ae"
},
{
"name": "microphone2",
"unicode": "1F399",
- "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32"
+ "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
},
{
"name": "studio_microphone",
"unicode": "1F399",
- "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32"
+ "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
},
{
"name": "microscope",
"unicode": "1F52C",
- "digest": "79918f5fe0a39f31f270a481f4c6e00ea49fc09d64b1ae78770971293c2b1ed8"
+ "digest": "4ca4322c6ba99b8c15acdb8b605f84f87398769e504b262b134c1f3868b2692f"
},
{
"name": "middle_finger",
"unicode": "1F595",
- "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6"
+ "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
},
{
"name": "reversed_hand_with_middle_finger_extended",
"unicode": "1F595",
- "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6"
+ "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
},
{
"name": "middle_finger_tone1",
"unicode": "1F595-1F3FB",
- "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448"
+ "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
},
{
"name": "reversed_hand_with_middle_finger_extended_tone1",
"unicode": "1F595-1F3FB",
- "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448"
+ "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
},
{
"name": "middle_finger_tone2",
"unicode": "1F595-1F3FC",
- "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f"
+ "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
},
{
"name": "reversed_hand_with_middle_finger_extended_tone2",
"unicode": "1F595-1F3FC",
- "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f"
+ "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
},
{
"name": "middle_finger_tone3",
"unicode": "1F595-1F3FD",
- "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da"
+ "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
},
{
"name": "reversed_hand_with_middle_finger_extended_tone3",
"unicode": "1F595-1F3FD",
- "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da"
+ "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
},
{
"name": "middle_finger_tone4",
"unicode": "1F595-1F3FE",
- "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04"
+ "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
},
{
"name": "reversed_hand_with_middle_finger_extended_tone4",
"unicode": "1F595-1F3FE",
- "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04"
+ "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
},
{
"name": "middle_finger_tone5",
"unicode": "1F595-1F3FF",
- "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664"
+ "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
},
{
"name": "reversed_hand_with_middle_finger_extended_tone5",
"unicode": "1F595-1F3FF",
- "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664"
+ "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
},
{
"name": "military_medal",
"unicode": "1F396",
- "digest": "bd1da0004768f404c6bb4db85d4b748f766a77ab3edb74e709d0c0064509a043"
+ "digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d"
},
{
"name": "milky_way",
"unicode": "1F30C",
- "digest": "598b4e641c1081bb03ce38a29f9711fc8616373216a833e4daa14fbe97a358f5"
+ "digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d"
},
{
"name": "minibus",
"unicode": "1F690",
- "digest": "3d15791ca96349c3abb5bd5d1014b6b33b984db19609f56f5fd1e8d2fc551809"
+ "digest": "08ccb4b1bf397b7c9aed901e2b5dcdd6cb8ca5c5487ef26775bb3120f7b92524"
},
{
"name": "minidisc",
"unicode": "1F4BD",
- "digest": "83c4bfda4e0a80785fa1c3f2bbf3c15aca2bda8ea3727ce78bc4236e1e377a36"
+ "digest": "bebf82c0b91ef66321e7ae7a0abf322e59b2f7d8e6fbf9a94243210c00229c59"
},
{
"name": "mobile_phone_off",
"unicode": "1F4F4",
- "digest": "cfe6dfd766b9e0b4768df25d6e943c9abc0e910ff5e5c7a8a0f425c786bbab8d"
+ "digest": "6f9d8d6a32fc998f5d8144a5ff7e2ad00de37ad464cd97285e7c72efb09a1feb"
},
{
"name": "money_mouth",
"unicode": "1F911",
- "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f"
+ "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
},
{
"name": "money_mouth_face",
"unicode": "1F911",
- "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f"
+ "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
},
{
"name": "money_with_wings",
"unicode": "1F4B8",
- "digest": "f7f1fa502d2f6804169869aeb5ca7f0ea64bc2d6a0204f08875d65da4f8cb332"
+ "digest": "15fcf0595021374ba091ca00efdb4167770da4d421eab930964108545f4edab9"
},
{
"name": "moneybag",
"unicode": "1F4B0",
- "digest": "442db49cda27360d2eb781489c9879730a6094c3267bb0a0a8687d84f8fed078"
+ "digest": "02d708e2f603b0df6f6c169b5c49b3452e1c02e7d72e96f228b73d0b0a20bff4"
},
{
"name": "monkey",
"unicode": "1F412",
- "digest": "3141c971aacbadaba21f970a515e192740212be2a49fa1f5eb0fc4dc576e209f"
+ "digest": "3588a544d6d9e9995b45d60327a1a42002fa1faa4d48224b140facd249af1c67"
},
{
"name": "monkey_face",
"unicode": "1F435",
- "digest": "e2397431d2befe44bf5298fa81d865d80722bf954113bceacc2aa98b84d856e2"
+ "digest": "9e263ef5ca42bb76d1b1d1e3cbf020bcf05023a6e9f91301d30c9eb406363a2a"
},
{
"name": "monorail",
"unicode": "1F69D",
- "digest": "b546153200d6fbe8d65b1b34f62ff4a19b1b6a159eb1b536c5c2ecb56dab0ec9"
+ "digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad"
},
{
"name": "mood_bubble",
@@ -7112,102 +7112,102 @@
{
"name": "mortar_board",
"unicode": "1F393",
- "digest": "cb59edb08f75c374088b65284e4d0f77b9bc9573de3e6a5127f865431011e54c"
+ "digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410"
},
{
"name": "mosque",
"unicode": "1F54C",
- "digest": "a08ddb74342dea8f79063db6f98ba03eb08fe99481de8ce9123827ca7f17c7f3"
+ "digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196"
},
{
"name": "motorboat",
"unicode": "1F6E5",
- "digest": "9dbea67bbe2e95dcc68c049a58f87390a44350b32308342615d75214af3d1cef"
+ "digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01"
},
{
"name": "motorcycle",
"unicode": "1F3CD",
- "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e"
+ "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
},
{
"name": "racing_motorcycle",
"unicode": "1F3CD",
- "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e"
+ "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
},
{
"name": "motorway",
"unicode": "1F6E3",
- "digest": "fc05a36c917637c135b0a60db8afcd58cee2b335070fe3888697f8026c9d11a5"
+ "digest": "148c3c13c7c4565453d16e504e0d4b8d007e4f2cad1ab56b1b51fefe39162d17"
},
{
"name": "mount_fuji",
"unicode": "1F5FB",
- "digest": "22bfffef033637b3c9b2fe7e539c74a659d2a49e594d2b33be894da00654d059"
+ "digest": "f8093b9dba62b22c6c88f137be88b2fd3971c560714db15ec053cf697a3820bc"
},
{
"name": "mountain",
"unicode": "26F0",
- "digest": "486cf4e9d5f3913d138fdb7878fe869b39caa3fca53876365957a89dc8f7edb8"
+ "digest": "07423804ad79da68f140948d29df193f5d5343b7b2c23758c086697c4d3a50da"
},
{
"name": "mountain_bicyclist",
"unicode": "1F6B5",
- "digest": "b547b96951b6837df8ae3be1e846f15e7e2ac06d976e1fe7f1442dcc5d3a0942"
+ "digest": "91084b6c887cb7e34f3d7ec30656ecb82c36cc987f53a6c83ccb4c6f7950f96a"
},
{
"name": "mountain_bicyclist_tone1",
"unicode": "1F6B5-1F3FB",
- "digest": "68ce0d55163c7b89ee1d87b752ece127bb25ca9deb3421b31df549a00ac5f69d"
+ "digest": "5d57fcfad61bca26c3e8965eb57602a1993a3117ebdda0f24569af730310ab6e"
},
{
"name": "mountain_bicyclist_tone2",
"unicode": "1F6B5-1F3FC",
- "digest": "5bfa82180bfb8bc4444cf301688aff02884895574a7ba66b398aaf20bde0f101"
+ "digest": "c0da7fb85d99aa01a665f64063cd7e2d994f8a16d3f6fbf52df5d471e771a98a"
},
{
"name": "mountain_bicyclist_tone3",
"unicode": "1F6B5-1F3FD",
- "digest": "33cb64a792123b81a05080465a0ea1035a2cdfdab01c71f5f725a5f92251c3e8"
+ "digest": "b099e7ee84eae44ebc99023fa06bdf37ffa0d69767c7c0163a89f7ced2a26765"
},
{
"name": "mountain_bicyclist_tone4",
"unicode": "1F6B5-1F3FE",
- "digest": "9c3fa4e65dcb0ad69b963292e77c7a75853ae3c1d18a90670f81ffb65b5d020c"
+ "digest": "9d09f7b3899ea44e736f237a161ef8d5170dccfa162a872c59532ceaf65ee007"
},
{
"name": "mountain_bicyclist_tone5",
"unicode": "1F6B5-1F3FF",
- "digest": "871de9e3fddb49b305e5f91000143878b0288c107a125c4e60acf2b6cf8b7f3f"
+ "digest": "71e374981d955056748a60c6d1820b45e9688a156b55318b4ea54a3a67ca801c"
},
{
"name": "mountain_cableway",
"unicode": "1F6A0",
- "digest": "f248ed5bf864f4a81e365b30d2825d2e6fc15a200c4ccf69e9f797341529f955"
+ "digest": "e261c3292758b1c0063c5a0d0c7f5c9803306d2265e08677027e1210506ced94"
},
{
"name": "mountain_railway",
"unicode": "1F69E",
- "digest": "7dd08745ab56c95c3dfcebcca517ff231cef61b670cedf9d7c53f3244c34e30b"
+ "digest": "b0987f8f391b3cbc7a56b9b8945ebfca240e01d12f8fd163877ebebe51d6b277"
},
{
"name": "mountain_snow",
"unicode": "1F3D4",
- "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff"
+ "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
},
{
"name": "snow_capped_mountain",
"unicode": "1F3D4",
- "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff"
+ "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
},
{
"name": "mouse",
"unicode": "1F42D",
- "digest": "fb20b3a82f407a6316bbbac68d58018c3d5b93a9a6ae968f44ace18d1c5698d9"
+ "digest": "007dd108507b45224f7a1fad3c1de6ecc75f38d71fc142744611eb13555f5eff"
},
{
"name": "mouse2",
"unicode": "1F401",
- "digest": "87be4099523ec32440e6d091f1193a8ed90730b9fbecaafed4912585bfe7818c"
+ "digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d"
},
{
"name": "mouse_one",
@@ -7222,132 +7222,132 @@
{
"name": "mouse_three_button",
"unicode": "1F5B1",
- "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01"
+ "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
},
{
"name": "three_button_mouse",
"unicode": "1F5B1",
- "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01"
+ "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
},
{
"name": "movie_camera",
"unicode": "1F3A5",
- "digest": "d6633b89a637b64d617c3032eed74bb82d3fa732dd9975486b2b5841b473808a"
+ "digest": "f7e285eda35b4431c07951e071643ddc34147cd76640e0d516fbfd11208346e9"
},
{
"name": "moyai",
"unicode": "1F5FF",
- "digest": "bf948c26cd98e2f5e48da363f2924a9d7c217232115a00cec372d0d5293402a8"
+ "digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb"
},
{
"name": "muscle",
"unicode": "1F4AA",
- "digest": "c85147efb786bdea3e7d53e2edf6b827280cd9fa881661a6102a614bf5b3579f"
+ "digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba"
},
{
"name": "muscle_tone1",
"unicode": "1F4AA-1F3FB",
- "digest": "38d071df2b25031b61f3605b03c34d2e5d3e35d29f3c4aada14be37e19750eb8"
+ "digest": "4a2fa226a05bb847b62cdd163eb6c2d514d3c2330a727991cf550c0d32b0e818"
},
{
"name": "muscle_tone2",
"unicode": "1F4AA-1F3FC",
- "digest": "dcf11b76c8ffb58dc7e4f9ecd32a4c291d9772d51df2853d41081e041e7e0876"
+ "digest": "a8d5ecce335c782ca5f5e55763c06cfefa1c16c24cd6602237cf125d4ff95e47"
},
{
"name": "muscle_tone3",
"unicode": "1F4AA-1F3FD",
- "digest": "a3d5f8f2dbfc28f9713ee657428ea3292c47d0b22f11a51c13594be22b0f5204"
+ "digest": "070354b443faec3969663b770545fc4cf5ec75148557b2b9d6fc82ab22b43bd1"
},
{
"name": "muscle_tone4",
"unicode": "1F4AA-1F3FE",
- "digest": "eb220fc19be58d16cacc6b721e1011078b03256c0245756f251a4c2bcf50586c"
+ "digest": "8eafcdb6a607aeafa673c257df0d2a1b20f00fc0868d811babcbe784490a0dd3"
},
{
"name": "muscle_tone5",
"unicode": "1F4AA-1F3FF",
- "digest": "4e18708cbd61eaad288f913c86ad2d45108dd4484bc35879c5dcdd075eeb09fd"
+ "digest": "85a1e2b5c89907694240e9c5b9d876a741fa7ba38918c5718273e289cbc40efe"
},
{
"name": "mushroom",
"unicode": "1F344",
- "digest": "a2b252cd759244409d9a8066470059948e2c50b8cc86b59821c1c86b5190f640"
+ "digest": "aaca8cf7c5cfa4487b5fef365a231f98be4bbf041197fc022161bcc8ce6f57c8"
},
{
"name": "musical_keyboard",
"unicode": "1F3B9",
- "digest": "dcb3e84d27bfe373e5ea7ede457908de52002f0fd6105e9f3f5525c54d2a43dd"
+ "digest": "fb0a726728900377d76d94aac9c94dce29107e8e3f1dcb0599d95bce7169b492"
},
{
"name": "musical_note",
"unicode": "1F3B5",
- "digest": "76a0f598f8e251a9dab44f2e14f2b7a6fb0c0c351e0f37862c8c99d380f1c261"
+ "digest": "41288e79b4070bb980281d0e0d1c14d8b144b4aedb2eaadb9f2bebcb4ef892b4"
},
{
"name": "musical_score",
"unicode": "1F3BC",
- "digest": "a132c6b35236005b45c830a42fa97b454d3061c14991c6320f34807f10ba6a4a"
+ "digest": "f0f91b9fa4a2bff7a5a1a11afa6f31cfe7e5fa8b0d6f3cce904b781a28ed0277"
},
{
"name": "mute",
"unicode": "1F507",
- "digest": "73a99b7f9e00f92cab78cd304dee4e893a112c3a6f2285c13d44916ea547458e"
+ "digest": "def277da49d744b55c7cdde269a15aa05315898f615e721ee7e9205d7b8030d6"
},
{
"name": "nail_care",
"unicode": "1F485",
- "digest": "62f721d3610d1647dba4b3f53cd4f2bc4180dae298314c2cca2a6a8ab1664525"
+ "digest": "48b33b1dbbd25b4f34ab2ca07bb99ddaaaa741990142c5623310f76b78c076f9"
},
{
"name": "nail_care_tone1",
"unicode": "1F485-1F3FB",
- "digest": "11b82ed2e6b6619c9b74702fdacfb0ddc91310191c8b89f355c7c69a72673f8f"
+ "digest": "a9ac92a34f407e7dd7c71377e6275e66657f7f42e4b911c540d1a66a02d92ac5"
},
{
"name": "nail_care_tone2",
"unicode": "1F485-1F3FC",
- "digest": "5195c76bccb9149d9080347d785dae2cce947bada5b198fae8c23e42f5553154"
+ "digest": "f295ec85980aaa75818fad619c3d25042146ecbbf361db9e9bb96e7bc202bc73"
},
{
"name": "nail_care_tone3",
"unicode": "1F485-1F3FD",
- "digest": "50eab0bf825c5e00db07a3f5ad26b1bb221f54efb5c55549f392b2f5aec09e5a"
+ "digest": "02ec373052a250977298bae85262177910126cc10de9480f1afa328ac2f65a95"
},
{
"name": "nail_care_tone4",
"unicode": "1F485-1F3FE",
- "digest": "d05a9ccfad02191c89e4cbd00aa48fdaf908c0de6681f4a587d500be448e528f"
+ "digest": "f3d95390ab59caedfda66122bbd0acf3aabedc142fc48352d68900766a7e6f5c"
},
{
"name": "nail_care_tone5",
"unicode": "1F485-1F3FF",
- "digest": "62466354dcf6717a8b9e942ca2c5ad15a26aa815c213e3b01faba9a2e302ecdd"
+ "digest": "009423c97f2aafd24fb8c7c485c58b30bbf9ae6797cc14b80d472b207327b518"
},
{
"name": "name_badge",
"unicode": "1F4DB",
- "digest": "0a1cb0f7d489d3356a4d3e01f9faf78449d82d8ec4595c8639a55c3606c97c40"
+ "digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628"
},
{
"name": "necktie",
"unicode": "1F454",
- "digest": "029e1140391ef559a9316021c2db94f05653751fdf9d8f366446467a70fee6df"
+ "digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68"
},
{
"name": "negative_squared_cross_mark",
"unicode": "274E",
- "digest": "0ba0e705fdeac99edd712db31a8846320b9d2cf53c9cb4d4bcfd22ba4e1488ea"
+ "digest": "1cdaf4abc9adafa089c91c2e33a24e9e647aea0f857e767941a899a16ec53b74"
},
{
"name": "nerd",
"unicode": "1F913",
- "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98"
+ "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
},
{
"name": "nerd_face",
"unicode": "1F913",
- "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98"
+ "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
},
{
"name": "network",
@@ -7362,157 +7362,157 @@
{
"name": "neutral_face",
"unicode": "1F610",
- "digest": "df01da8501e1f588049c8ed66e504e9abcce83f74ce5790f4d3dc547408f77ee"
+ "digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e"
},
{
"name": "new",
"unicode": "1F195",
- "digest": "24e80abd29750d8b297335cdd4751b6250bb820560cf0392a6cc8783d34db63a"
+ "digest": "e20bc3e9f40726afd0cfb7268d02f1e1a07343364fd08b252d59f38de067bf06"
},
{
"name": "new_moon",
"unicode": "1F311",
- "digest": "2d697e431eac53d6e1ea367b5da03c15fc535cd7e8c214f801fe595b768a8e11"
+ "digest": "dbfc5dcae34b45f15ff767e297cba3a12cb83f3b542db8cfc8dbd9669e0df46c"
},
{
"name": "new_moon_with_face",
"unicode": "1F31A",
- "digest": "ea469a4668ded071f35e5898ae229fdb5d02b0730ce233169b83e22f81292baa"
+ "digest": "c66d347d2222ac8d77d323a07699aff6b168328648db4f885b1ed0e2831fd59b"
},
{
"name": "newspaper",
"unicode": "1F4F0",
- "digest": "0aaf6747a43fb60cd15e6e64ca0eccaade331b376c6fe6712fd5e8294e9868cc"
+ "digest": "c05e986d9cdac11afa30c6a21a72572ddf50fc64e87ae0c4e0ad57ffe70acc5c"
},
{
"name": "newspaper2",
"unicode": "1F5DE",
- "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf"
+ "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
},
{
"name": "rolled_up_newspaper",
"unicode": "1F5DE",
- "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf"
+ "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
},
{
"name": "ng",
"unicode": "1F196",
- "digest": "4994c9b795033ed788e98c4af571a1dffe28c0a1479e3b42dcae21bb08381b5f"
+ "digest": "34d5a11c70f48ea719e602908534f446b192622e775d4160f0e1ec52c342a35c"
},
{
"name": "night_with_stars",
"unicode": "1F303",
- "digest": "56bb4a59a897c1836ee1a49cc99f468891b790b0f8bce203c201c13bb7b8ae9a"
+ "digest": "39d9c079be80ee6ce1667531be528a2aa7f8bd46c7b6c2a6ee279d9a207c84a4"
},
{
"name": "nine",
"unicode": "0039-20E3",
- "digest": "7e3644a98cb6417a351530c9ce6b368e637a22c847a8c04133897dc1c5d7419f"
+ "digest": "8bb40750eda8506ef877c9a3b8e2039d26f20eef345742f635740574a7e8daa6"
},
{
"name": "no_bell",
"unicode": "1F515",
- "digest": "f4fb42836132000101624fecef8b9358736a0fc76beae460e6986aaa479204fd"
+ "digest": "6542a9a5656c79c153f8c37f12d48f677c89b02ed0989ae37fa5e51ce6895422"
},
{
"name": "no_bicycles",
"unicode": "1F6B3",
- "digest": "b3c258bea7d6988640e3348598c03c97632ca00a11cbf0352995b801ff4a296b"
+ "digest": "af71c183545da2ff4c05609f9d572edb64b63ccba7c6a4b208d271558aa92b0a"
},
{
"name": "no_entry",
"unicode": "26D4",
- "digest": "ac807d54092efdc3aea417790a7d0c50b59800c9ea49b37f1aec6d2e453c5f6d"
+ "digest": "dc0bac1ed9ab8e9af143f0fce5043fe68f7f46bd80856cdec95d20c3999b637d"
},
{
"name": "no_entry_sign",
"unicode": "1F6AB",
- "digest": "5a17d677ec1c7595a7970a1cbe0d20909341b30d3ab31471ced590f51fff1ff7"
+ "digest": "2c1fceef23b62effca68e0e087b8f020125d25b98d61492b1540055d1914fdc3"
},
{
"name": "no_good",
"unicode": "1F645",
- "digest": "8ce921e5e13e1203cf43fdc3e7c5ec1fb2a1f9ff79f21539cff542c80af2e5fe"
+ "digest": "6eb970b104389be5d18657d7c04be5149958c26855c52ea68574af852c5f85c4"
},
{
"name": "no_good_tone1",
"unicode": "1F645-1F3FB",
- "digest": "aab4d354aaac06e8348eb354487c6381e475b44651cb2716660904a36c47a1b6"
+ "digest": "c20a24a1e536240b4dcf90ecb530796de621d7ba1fb9e3fa0f849d048c509c03"
},
{
"name": "no_good_tone2",
"unicode": "1F645-1F3FC",
- "digest": "8fb66b1a7b8f72062794281294515d47471a8c59de300b99d656c3412ca19d64"
+ "digest": "f31a4628c1f2e6a39288fda8eb19c9ec89983e3726e17a09384d9ecc13ef0b4c"
},
{
"name": "no_good_tone3",
"unicode": "1F645-1F3FD",
- "digest": "aeecf73fb9dca24b4002db2802fc9b5a483644c49f834c19f143d4e56ec46c1a"
+ "digest": "959dec1bfdaf37b20a86ab2bcbdbacd3179c87b163042377d966eab47564c0fb"
},
{
"name": "no_good_tone4",
"unicode": "1F645-1F3FE",
- "digest": "fadeb23307d5ccabbf08c848cf81c66c05b152aa32b85f86061caf14760f8eb9"
+ "digest": "efd931f0080adf2e04129c83a8b24fda0ae7a9fa7c4b463686c0b99023620db8"
},
{
"name": "no_good_tone5",
"unicode": "1F645-1F3FF",
- "digest": "cf26d5d6463d0febf4e1f08e343308742ffe0811cfc30c459b87d4cc812f5d04"
+ "digest": "f35df2b26af9baef47c1f8cc97a1b28a58aa7fcb2a13fdac7b2d9189f1e40105"
},
{
"name": "no_mobile_phones",
"unicode": "1F4F5",
- "digest": "3b4ead88beca33f1e303d0a45268849be7aaaff7830b761732c7a5afc5a2de3a"
+ "digest": "a472decd6ac7f9777961c09e00458746b2c04965585e3bee4556be3968e55bcd"
},
{
"name": "no_mouth",
"unicode": "1F636",
- "digest": "2af81a3e07a8b7827a1e58f6f5036ccff2f6e7b0027a4f934c9fa34c6a780963"
+ "digest": "72dda8b1e3ad4b05d9b095f9bd05e95d7ba013906c68914976a4554e8edf5866"
},
{
"name": "no_pedestrians",
"unicode": "1F6B7",
- "digest": "9f9ed90bb8f9964fa8cb0048fc092ecc0612a1994c98d19ef0b5a58607888476"
+ "digest": "062b4a71b338fe09775e465bfba8ac04efbb3640330e8cabe88f3af62b0f4225"
},
{
"name": "no_smoking",
"unicode": "1F6AD",
- "digest": "fb90290ff5c917b7307a97c8ba793d20c61042525cf2f7bfd4cd2a7878aeefa5"
+ "digest": "ae2ebb331f79f6074091c0ee9cd69fce16d5e12a131d18973fc05520097e14ee"
},
{
"name": "non-potable_water",
"unicode": "1F6B1",
- "digest": "c4ddca2ab1a97260e9b2c2aa33fb03455c0e8174541c3a9416fc44143a3ee567"
+ "digest": "32eba0a99b498133c2e4450036f768d3dccaaf5b50adc9ad988757adc777a6a1"
},
{
"name": "nose",
"unicode": "1F443",
- "digest": "308e28b15b7f734f6f184ae367789d7cf258656b24861cf8d5935127d810aa3f"
+ "digest": "9f800e24658ea3cebe1144d5d808cf13a88261f1a7f1f81a10d03b3d9d00e541"
},
{
"name": "nose_tone1",
"unicode": "1F443-1F3FB",
- "digest": "392d24b38ac3edc2d7b83945a60bbe9115a6a97658d8af35281a7cbef79449e8"
+ "digest": "a2d0af22284b1d264eb780943b8360f463996a5c9c9584b8473edf8d442d9173"
},
{
"name": "nose_tone2",
"unicode": "1F443-1F3FC",
- "digest": "409a790339c405770492e49fdb0b5ba34087c27e2f9018ecd845ab078e61476a"
+ "digest": "244dcaa8540024cf521f29f36bd48f933bf82f4833e35e6fa0abf113022038f3"
},
{
"name": "nose_tone3",
"unicode": "1F443-1F3FD",
- "digest": "92b52b479a935f31e460257d809c531edad1a6bb4583ad18233b12c4e45202fe"
+ "digest": "c935b64866f0d49da52035aa09f36ff56d238eb7f5b92205386451056e8ea74f"
},
{
"name": "nose_tone4",
"unicode": "1F443-1F3FE",
- "digest": "78ad2e857792e86cded6ba5620f634f7d1f79a92c82c266e48fab9bd73df3688"
+ "digest": "a87e95fd9319c49e66b6dea0e57319d0ed9921b8d94df037767bf3d5dc7c94f3"
},
{
"name": "nose_tone5",
"unicode": "1F443-1F3FF",
- "digest": "dbef6813c1965d3e93f70f33f118f9950130af21c622cea97ea215a36b4fa73f"
+ "digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60"
},
{
"name": "note",
@@ -7537,12 +7537,12 @@
{
"name": "notebook",
"unicode": "1F4D3",
- "digest": "64bd4a3e7ca7b22fc704c7b7bd4d13540c16bc69b9d8dd76e69e6ad573ab3823"
+ "digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8"
},
{
"name": "notebook_with_decorative_cover",
"unicode": "1F4D4",
- "digest": "4b45f28fbde1be5c214a6bc2413abc91db02bccd86f74c21b7f4a4da8b75a46f"
+ "digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef"
},
{
"name": "notepad",
@@ -7567,297 +7567,297 @@
{
"name": "notepad_spiral",
"unicode": "1F5D2",
- "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727"
+ "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
},
{
"name": "spiral_note_pad",
"unicode": "1F5D2",
- "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727"
+ "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
},
{
"name": "notes",
"unicode": "1F3B6",
- "digest": "bf3868386e17eac40ac7fbabea027042027ff061daafe406c869cdd8ce94641d"
+ "digest": "98467e0adc134d45676ef1c6c459e5853a9db50c8a6e91b6aec7d449aa737f48"
},
{
"name": "nut_and_bolt",
"unicode": "1F529",
- "digest": "fdb9d7408202fad7a52ff21608042c08c3b0beb195999fff233df36a29dc9e96"
+ "digest": "a77bd72f29a7302195dcec240174b15586de79e3204258e3fb401a6ea90563b3"
},
{
"name": "o",
"unicode": "2B55",
- "digest": "8e119dba4130bd33b3ee5c862fb4fa5a691173911ffee51cb9359fee3398e330"
+ "digest": "2387e5fd9ae4c2972d40298d32319b8fa55c50dbfc1c04c5c36088213e6951dd"
},
{
"name": "o2",
"unicode": "1F17E",
- "digest": "00d751124c25633611055bd61e74fc3f3d1779f0d09e1e707837686f613367b4"
+ "digest": "6a9ccb0bf394e4d05ffda19327cee18f7b9ed80367fc7f41c93da9bb7efab0bf"
},
{
"name": "ocean",
"unicode": "1F30A",
- "digest": "9b1fbfd2a64f417d0c2cb91085b29a12d14e15844bc21798bdee938bb7bf6222"
+ "digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e"
},
{
"name": "octopus",
"unicode": "1F419",
- "digest": "3fdfbc02f47ad434bdeb7f3a15cd4e8f8118ee1cd754627e358f1c2f4616f5e3"
+ "digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59"
},
{
"name": "oden",
"unicode": "1F362",
- "digest": "afed1c5166943e5803602ffacc67652e3b29ee4222a6c36aba2daf88bd21ad3c"
+ "digest": "089974cb13a0bef6a245fc73029c5ed5153fd4caae0177b835f779e32200b8aa"
},
{
"name": "office",
"unicode": "1F3E2",
- "digest": "dc1836ef152d88fd628df18db770594f5dbc8d7f20d6ce982588b25b78b19c92"
+ "digest": "3633a2e91036362e273eef4e0cfbdbbb4cb1208afe2cfa110ebef7b78109a66f"
},
{
"name": "oil",
"unicode": "1F6E2",
- "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a"
+ "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
},
{
"name": "oil_drum",
"unicode": "1F6E2",
- "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a"
+ "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
},
{
"name": "ok",
"unicode": "1F197",
- "digest": "6b05bbab4a7104541c2f4bce553884d17ae0ad07589b19d6b53b6949c14f2269"
+ "digest": "5f320f9b96e98a2f17ebe240daff9b9fd2ae0727cd6c8e4633b1744356e89365"
},
{
"name": "ok_hand",
"unicode": "1F44C",
- "digest": "9981f32ef200b011a10f6bfa2066c41b6b5e7bcd6c3c21647980b640bc1fa93b"
+ "digest": "d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d"
},
{
"name": "ok_hand_tone1",
"unicode": "1F44C-1F3FB",
- "digest": "e5933a9b64b03ce0634f15f02ff7b6424530dbdc0e283461e0c9992d0c2ca2ad"
+ "digest": "ef1508efcf483b09807554fe0e451c2948224f9deb85463e8e0dad6875b54012"
},
{
"name": "ok_hand_tone2",
"unicode": "1F44C-1F3FC",
- "digest": "4c04741c9f2c8731da8df3015e9aae00061a01848c2d22aab1e9853c271deed3"
+ "digest": "1215a101a082fd8e04c5d2f7e3c59d0f480cb0bedd79aeab5d36676bfe760088"
},
{
"name": "ok_hand_tone3",
"unicode": "1F44C-1F3FD",
- "digest": "216dc5a72f9e34bbb7b39f680c388bd5b52abf9b41b843342e53e285b7933076"
+ "digest": "6fe0ed9fb42e86bb2bed4cb37b2acacacda1471fb1ee845ad55e54fb0897fbf4"
},
{
"name": "ok_hand_tone4",
"unicode": "1F44C-1F3FE",
- "digest": "7139de7ec9d5a962cf87b9fbbeef3a53aa482bb840ab3b64d8d0da81bdc19886"
+ "digest": "bfb9041c49d95e901a667264abaf9b398f6c4aa8b52bf5191c122db20c13c020"
},
{
"name": "ok_hand_tone5",
"unicode": "1F44C-1F3FF",
- "digest": "e18b0a1bc5d970cc63466bd6da6e9f855db37d1eada3230d19f600c1f5a402a3"
+ "digest": "1c218dc04d698da2cbdd7bea1ca3f845f9b386e967b7247c52f4b0f6ec8f5320"
},
{
"name": "ok_woman",
"unicode": "1F646",
- "digest": "3b2fa732d9c9addb056f136192428e99d805d4cb1c7dab724fd552c7e93197e4"
+ "digest": "3f8bd4ce2c4497155d697e5a71ebdc9339f65633d07fa9a7903e1bd76cfa4ba1"
},
{
"name": "ok_woman_tone1",
"unicode": "1F646-1F3FB",
- "digest": "017aca3797701b043a44f22e67dcad8b531a3ca14e629ae0d2fbc601ed3e49cb"
+ "digest": "1660cd904ccd2ecdc6f4ba00527f7d4ec8c33f3c6183344616f97badae4c3730"
},
{
"name": "ok_woman_tone2",
"unicode": "1F646-1F3FC",
- "digest": "036bed032bc5a616668775cda0d5640c810e2836aa28009c8e8bf2b487259c59"
+ "digest": "7ba5fddd1e141424fac6778894dfc5af28e125839c58937c69496f99cd2c4002"
},
{
"name": "ok_woman_tone3",
"unicode": "1F646-1F3FD",
- "digest": "d9a4414caddda43d1a36828cfbecce5f2b7e5c1b67b4a47991b2ae0a34cf7ab7"
+ "digest": "1d972b8377c52f598406f59ab1e5be41aaf8f027e1fefba3deda66312ccd6a9b"
},
{
"name": "ok_woman_tone4",
"unicode": "1F646-1F3FE",
- "digest": "942e1b9aa495c4c4de0804e4d4348422201299d649e5d65829ba4a308880df1c"
+ "digest": "a176328d8f53503aa743448968afd21d72ffd3510555526a3fb38d6b30ee7c15"
},
{
"name": "ok_woman_tone5",
"unicode": "1F646-1F3FF",
- "digest": "e8d0fb5b999d5d63404493aa505b5af2260c76001023431d5e788773d0a9e2de"
+ "digest": "13cfc1b589c57e81f768ee07a14b737cafc71407a7eb0956728b2ec4b1df14c4"
},
{
"name": "older_man",
"unicode": "1F474",
- "digest": "620f763325827acbeb9d57798ef55d87827d0dfc77b84d942e25bc5057f2cbfe"
+ "digest": "4c0462b199bf26181c9e4d2d4cb878a32b0294566941212efc67362d0645f948"
},
{
"name": "older_man_tone1",
"unicode": "1F474-1F3FB",
- "digest": "e0f35c12362eae503d1c30a345c3a4978196d351d8a1eb9d5f107c60ea4bbf52"
+ "digest": "99baa083f78cb01166d0a928d0b53682be14be04c29fc17bef14aac1a73a61e6"
},
{
"name": "older_man_tone2",
"unicode": "1F474-1F3FC",
- "digest": "671766ce9fa47c3fa009d4f138344c87d73032a1c38e48614c663f8ea5d0f673"
+ "digest": "5b4ce713e8820ba517fe92c25f3b93e6a6bf3704d1f982c461d5f31fc02b9d3d"
},
{
"name": "older_man_tone3",
"unicode": "1F474-1F3FD",
- "digest": "6ff4885ef8c416b8970780a691fef74c8d89421ab11e0aa8c522c33e1c67fbe8"
+ "digest": "0eff72b3226c3a703c635798ee84129a695c896fa011fe1adbc105312eecc083"
},
{
"name": "older_man_tone4",
"unicode": "1F474-1F3FE",
- "digest": "0ae7d4e316dcd4d27a5a6cdaabab88a4f992bd1b75f6ceaeb5b906ed1eb5269c"
+ "digest": "ad9ba82b0c5d3b171b0639ee4265370dbddff5e0eeb70729db122659bb8c8f84"
},
{
"name": "older_man_tone5",
"unicode": "1F474-1F3FF",
- "digest": "abe2757bd5e35f30d2a6daec09637ea5382a46d14d239b77282e9bf874229b57"
+ "digest": "5eb0a7467cc40e75752e11fd5126b275863dc037557a0d0d3b24b681e00c2386"
},
{
"name": "older_woman",
"unicode": "1F475",
- "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c"
+ "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
},
{
"name": "grandma",
"unicode": "1F475",
- "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c"
+ "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
},
{
"name": "older_woman_tone1",
"unicode": "1F475-1F3FB",
- "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda"
+ "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
},
{
"name": "grandma_tone1",
"unicode": "1F475-1F3FB",
- "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda"
+ "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
},
{
"name": "older_woman_tone2",
"unicode": "1F475-1F3FC",
- "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde"
+ "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
},
{
"name": "grandma_tone2",
"unicode": "1F475-1F3FC",
- "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde"
+ "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
},
{
"name": "older_woman_tone3",
"unicode": "1F475-1F3FD",
- "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269"
+ "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
},
{
"name": "grandma_tone3",
"unicode": "1F475-1F3FD",
- "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269"
+ "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
},
{
"name": "older_woman_tone4",
"unicode": "1F475-1F3FE",
- "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c"
+ "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
},
{
"name": "grandma_tone4",
"unicode": "1F475-1F3FE",
- "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c"
+ "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
},
{
"name": "older_woman_tone5",
"unicode": "1F475-1F3FF",
- "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b"
+ "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
},
{
"name": "grandma_tone5",
"unicode": "1F475-1F3FF",
- "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b"
+ "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
},
{
"name": "om_symbol",
"unicode": "1F549",
- "digest": "c8c1c9d445b1fc50a627b71bee21fba978e04532e4685ec032a0174f51fc12bb"
+ "digest": "5ead73bea546ba9ba6da522f7280cc289c75ff5467742bdba31f92d0e1b3f4e6"
},
{
"name": "on",
"unicode": "1F51B",
- "digest": "08e1159a68d3334a87ffa75b9e70826cb557d0f73a2c1d08f4c3d60476ecacc8"
+ "digest": "9cc61a6b31a30c32dab594191bf23f91e341c4105384ab22158a6d43e6364631"
},
{
"name": "oncoming_automobile",
"unicode": "1F698",
- "digest": "6bff7f40fe223df6d16c7512532b8aa6f83e8c13e1007b63eb9aabf774c1a322"
+ "digest": "557c9cacdc3f95215d4f7a6f097a2baa7c007cb9c519492a6717077af4ca6b56"
},
{
"name": "oncoming_bus",
"unicode": "1F68D",
- "digest": "127a357fcd96ce4b9ab11c3dba95d8ff811bab193dd8ba38efb7067a44752ce8"
+ "digest": "059f28ce6bfb337e107db5982cbd2004844450ef20b4a54b9ca3cb738360ab05"
},
{
"name": "oncoming_police_car",
"unicode": "1F694",
- "digest": "57cb70e05e70c1f68ab42259f307ed9782c2b9d6e35d2dff2895aa23d7eb6b04"
+ "digest": "aee79306a0d129cfc1980f58db80391eb46d2d7d5f814bf431414dc7680cab72"
},
{
"name": "oncoming_taxi",
"unicode": "1F696",
- "digest": "174967ae4c3d5881d2408c71c020f704e933190af4caef5d2908e9ac382f35ea"
+ "digest": "84351489fc86d980b8d3eb9ec4e81120fe700b3ac01346daebe2b7aeb9607a55"
},
{
"name": "one",
"unicode": "0031-20E3",
- "digest": "113b9d87c3e37c9c54e49cecccbfc40c15fb97fd03a51505df85e48b78702b2b"
+ "digest": "d5d3fff04e68a114ff6464ee06fc831f3f381713045165f62a88d5e8215c195b"
},
{
"name": "open_file_folder",
"unicode": "1F4C2",
- "digest": "def93715203aed464211798d773732895a19389a94a2e7ed43e7f229b2aab7da"
+ "digest": "96cfc322ee4903ae8cec07604811742245fd7d14f00bb70276d39d29c48bed28"
},
{
"name": "open_hands",
"unicode": "1F450",
- "digest": "7c60a37ae11727c998908199b8709e52593b931843aef942f37b306b1edca12a"
+ "digest": "a6c131da2040b48103cea14f280e728675da50fa448d2b3f3438fcbb5bf5596a"
},
{
"name": "open_hands_tone1",
"unicode": "1F450-1F3FB",
- "digest": "09ffa9b3f28fc56a71e4e711bdfc87ce1a56721229377e71f1c00224523f8b9b"
+ "digest": "867128dff2fa9b860c10c6b792f989f0c057928783696062378f834c0ef89d85"
},
{
"name": "open_hands_tone2",
"unicode": "1F450-1F3FC",
- "digest": "21ecaba9f086bcb7eb07c17c2b2621bcd1ca28c57f79032d5e0eba356494cc85"
+ "digest": "487ff2745b03d49bb3b1d0acd86ba530fd8cc3f467ca3fa504f88f0ef1cbbc01"
},
{
"name": "open_hands_tone3",
"unicode": "1F450-1F3FD",
- "digest": "c7dbb8c44f78f7793b202ec215fee42b7e1e555d659fbf402383500217b89656"
+ "digest": "cb8cddc8b8661f874ac9478289d16cc41406b947bb87f3363df518a588a53e16"
},
{
"name": "open_hands_tone4",
"unicode": "1F450-1F3FE",
- "digest": "867451d42492ab2277687447f421f744530b9ea057312326353fec39c94b18fd"
+ "digest": "17dcc2c07230846a769f3c79ce618a757c88b9b58c95c6c5b2d7f968814d447d"
},
{
"name": "open_hands_tone5",
"unicode": "1F450-1F3FF",
- "digest": "56335506cf68e29150cb68d7ebbb4a92aed390018966669a8144d20ae0d6cfe3"
+ "digest": "36b2493d67c84cea4f3f85a3088c6abcfd35cf99f7aeaeedfafa420ee878e3d2"
},
{
"name": "open_mouth",
"unicode": "1F62E",
- "digest": "f05fdf998e8b5c0b00ebd8b5ab17a67f5c0a45275f31a201af74e8ab0c2f7ba9"
+ "digest": "1906c5100ae0c8326ca5c4f9422976958a38dadd8d77724d68538a25d9623035"
},
{
"name": "ophiuchus",
"unicode": "26CE",
- "digest": "98c61bb0c36d60c476d42d5e074297662e8d141dcab7004a5bd63c359eed3b84"
+ "digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b"
},
{
"name": "optical_disk",
@@ -7872,27 +7872,27 @@
{
"name": "orange_book",
"unicode": "1F4D9",
- "digest": "86d150ea3d62183ab7dfe2851cf7f4d1ae769b7ecbb1987b0f463e639e429598"
+ "digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf"
},
{
"name": "orthodox_cross",
"unicode": "2626",
- "digest": "9c861285ca6d699cd2c72b6df44ec2b1e64138152f19c66e32df1ce770ff2e83"
+ "digest": "c16372102f0169dd6d32eb2b27a633aaee74e4e0fddcf723c15ad97f9dc6075c"
},
{
"name": "outbox_tray",
"unicode": "1F4E4",
- "digest": "b6a6015d5d7d528af485de23ff4518dc35408def1cc49bc6c9b01d880d613985"
+ "digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf"
},
{
"name": "ox",
"unicode": "1F402",
- "digest": "cbcfe5c8c4d6b939e24e18e610785f171bb9410441e02c2eeb1bceb0a6246daf"
+ "digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed"
},
{
"name": "package",
"unicode": "1F4E6",
- "digest": "4023cffce85384217a73609f457aec013876e689c44bcfff0bcc35f3e4e1ab00"
+ "digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c"
},
{
"name": "page",
@@ -7902,17 +7902,17 @@
{
"name": "page_facing_up",
"unicode": "1F4C4",
- "digest": "71a0872bf1b13c58746f9b41655227c75be107ab6083c0dce13cb16444af22e7"
+ "digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a"
},
{
"name": "page_with_curl",
"unicode": "1F4C3",
- "digest": "cb4210464faea946c7b07db7067c7fc98920f778cf57721388f5362942ba3029"
+ "digest": "3d6257670189f841ad1fa45415c34feb2433b2cb35bb435c4ee122ce89b39669"
},
{
"name": "pager",
"unicode": "1F4DF",
- "digest": "209dbdc19aa650ecacc0569e17a9123c9a1e39df59c9b4120f3b0888b63cd6f1"
+ "digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12"
},
{
"name": "pages",
@@ -7922,132 +7922,132 @@
{
"name": "paintbrush",
"unicode": "1F58C",
- "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9"
+ "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
},
{
"name": "lower_left_paintbrush",
"unicode": "1F58C",
- "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9"
+ "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
},
{
"name": "palm_tree",
"unicode": "1F334",
- "digest": "1589ff4b1b87296edc0118e4aa67b3b504ed85a5b8d47e7d0c3e309d0bbf8cd6"
+ "digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1"
},
{
"name": "panda_face",
"unicode": "1F43C",
- "digest": "050ee87892f56ff485f460bc6c3846d98a0ca7083d2cf0b8ab24772b672273f2"
+ "digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b"
},
{
"name": "paperclip",
"unicode": "1F4CE",
- "digest": "1463607a59345973f009fa53a719e2264b95743560adb99737bef29b1d133a95"
+ "digest": "d1e2ce94a12b7e8b7a9bba49e47ddc7432ec0288545d3b6817c7a499e806e3f0"
},
{
"name": "paperclips",
"unicode": "1F587",
- "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040"
+ "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
},
{
"name": "linked_paperclips",
"unicode": "1F587",
- "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040"
+ "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
},
{
"name": "park",
"unicode": "1F3DE",
- "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50"
+ "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
},
{
"name": "national_park",
"unicode": "1F3DE",
- "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50"
+ "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
},
{
"name": "parking",
"unicode": "1F17F",
- "digest": "e1d2cfd1c57ea85003ca4df066cbba4e506bf6c4d6c790e27b2f78ad8443fabf"
+ "digest": "9f1da460a7dd58b26beab8cf701be2691fb812208fbc941c71daa35be1507c2f"
},
{
"name": "part_alternation_mark",
"unicode": "303D",
- "digest": "b3cc2e803b255e858417345ba6ba52a1c22f511b483fec11b5d68c4432f759b6"
+ "digest": "956da19353bb38fd4dfe0ab5360679a9035d566858fb5de62887b85c75fb8eef"
},
{
"name": "partly_sunny",
"unicode": "26C5",
- "digest": "484990f5e1a3b14c731e7bd4b0b4a1c10cd5fb54ac7cf2751f40c8bf59d7e2b4"
+ "digest": "8fb9a6d2caf9e0cce58447762f0dfd6aa0b581b2e83fea6411348e0cbc8cf3c4"
},
{
"name": "passport_control",
"unicode": "1F6C2",
- "digest": "224e8ef60d4d6587721727555de324948fb5b6c1cb5cc4b546960983d1ec85c4"
+ "digest": "d9be6eed2c90e1c89171c42d70a06485fdf86a4c68833371832cc1f6897fadd0"
},
{
"name": "pause_button",
"unicode": "23F8",
- "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272"
+ "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
},
{
"name": "double_vertical_bar",
"unicode": "23F8",
- "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272"
+ "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
},
{
"name": "peace",
"unicode": "262E",
- "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc"
+ "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
},
{
"name": "peace_symbol",
"unicode": "262E",
- "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc"
+ "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
},
{
"name": "peach",
"unicode": "1F351",
- "digest": "a3f4fd5ff02e0a03104ab54456ee1a7521858ee68443856ee10e0972e5b6aaa5"
+ "digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311"
},
{
"name": "pear",
"unicode": "1F350",
- "digest": "7a7a72568d53677cd1fff4d9e58e63327a742fa16d22a2bef03b4a6fa378d3b3"
+ "digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948"
},
{
"name": "pen_ballpoint",
"unicode": "1F58A",
- "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb"
+ "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
},
{
"name": "lower_left_ballpoint_pen",
"unicode": "1F58A",
- "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb"
+ "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
},
{
"name": "pen_fountain",
"unicode": "1F58B",
- "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a"
+ "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
},
{
"name": "lower_left_fountain_pen",
"unicode": "1F58B",
- "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a"
+ "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
},
{
"name": "pencil",
"unicode": "1F4DD",
- "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249"
+ "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
},
{
"name": "memo",
"unicode": "1F4DD",
- "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249"
+ "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
},
{
"name": "pencil2",
"unicode": "270F",
- "digest": "aa2c572772187fee1f9125bb0950f5ce8a61f7dd2647258c40b4077ee5feb498"
+ "digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0"
},
{
"name": "pencil3",
@@ -8062,7 +8062,7 @@
{
"name": "penguin",
"unicode": "1F427",
- "digest": "095de34b3f6a2521a342c21f5f2551a0092bf47429801c15b7bbf0913924f412"
+ "digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316"
},
{
"name": "pennant_black",
@@ -8087,147 +8087,147 @@
{
"name": "pensive",
"unicode": "1F614",
- "digest": "2d9e7f1eed14dcc86674cec78e992567a40d0f223fc67d722b91eebcd1251269"
+ "digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2"
},
{
"name": "performing_arts",
"unicode": "1F3AD",
- "digest": "a202755bab6427433975589bb8b63e61e5d7f55c6242676d8000e91eedabc55e"
+ "digest": "d7c7bc9213e308ca26286cbbd8012e656b0f9b00293758faf1bfccc4c5ceabed"
},
{
"name": "persevere",
"unicode": "1F623",
- "digest": "686ef3fc70ce8294d02a764ebd75b69f25cca6bff6b92e7905130366d22f6d8a"
+ "digest": "c361509c9b8663af19a02a1ffff61b1b0d0b4bd75d693ce3d406b0ca1bde1ca0"
},
{
"name": "person_frowning",
"unicode": "1F64D",
- "digest": "16e8fbf22c0b4c237d0d45202fa32d1ebd04760a5b6975c9c9b477321ccb0e12"
+ "digest": "b37be8bd95f21a6860ad3f171b8086125ab37331b382d87bcdb4cd684800546b"
},
{
"name": "person_frowning_tone1",
"unicode": "1F64D-1F3FB",
- "digest": "a143b865976ce3cf307db854cfd1ca58c3832df0eee5e9b0ab307cf4f24ba3db"
+ "digest": "3d5e78a367f9673baed2a86bc11cf04fd44394aadb65291fa51ade8dca318427"
},
{
"name": "person_frowning_tone2",
"unicode": "1F64D-1F3FC",
- "digest": "4e7050d8a38019ba2293f66b9930e6a7e35dacf3b3bc9431edb586a0d9ea8054"
+ "digest": "7456c414c65ad6b6f11855f68a2eedc18113526f86862c4373202397cb1bed2c"
},
{
"name": "person_frowning_tone3",
"unicode": "1F64D-1F3FD",
- "digest": "0750015d3ac1b5954d31e36cd59c70b6ed9f4df698082484b7ac59eb0b9964b0"
+ "digest": "c86cf2d6951f1e6a7c786a74caaf68a777cf00e88023e23849d4383f864ae437"
},
{
"name": "person_frowning_tone4",
"unicode": "1F64D-1F3FE",
- "digest": "18d6cc92d0990624218d38d6eeed60bccb371d0fc9f1c889e9476b3b0c44b5e8"
+ "digest": "944e96ced645ced8db6bb50120c7e37ed46b6960d595cbfe964c81803efa83aa"
},
{
"name": "person_frowning_tone5",
"unicode": "1F64D-1F3FF",
- "digest": "4a898199cbaf083d37511f51d8a1d2560b7a20c62a1b09087831da7010fbd093"
+ "digest": "4bd0ea571be6ef9f0493784ef0d12d5e47bc2d6ac610fb42c450bf3d87fb2948"
},
{
"name": "person_with_blond_hair",
"unicode": "1F471",
- "digest": "67d95a0801c65f62db55fa80ab35dec65c239601a44bf5f5902e4645f126770e"
+ "digest": "a7f94ede2e43308108c2260d83fc10121dda09a67f94a0a840e6d7bba7fd5616"
},
{
"name": "person_with_blond_hair_tone1",
"unicode": "1F471-1F3FB",
- "digest": "e79717bfe30a26eafc082a75fa7547d8f2ad3c123fb2d75a95e75f0ce7ecbd0c"
+ "digest": "00a116357a7878554c83e5bade4bddfa9cfabf76a229efa19cbb58e0d216219c"
},
{
"name": "person_with_blond_hair_tone2",
"unicode": "1F471-1F3FC",
- "digest": "c4a1961c292149ab6e1fd54a7894398599bf855de97a05ee4e836a86a400deb3"
+ "digest": "df509ebe92ed3138b9d5bd4645eff4b13f77f714cf62bb949c59eff1adc00019"
},
{
"name": "person_with_blond_hair_tone3",
"unicode": "1F471-1F3FD",
- "digest": "e2707d0cf778bee5b72d861ec76430eb1cf9f9820f066ee6327574d5697f445e"
+ "digest": "6f328513f440a0c8cd1dc44596a5028fd8f306bdaf57c1e6f3aa94a3aa262b3c"
},
{
"name": "person_with_blond_hair_tone4",
"unicode": "1F471-1F3FE",
- "digest": "94da43f0b12ef4a98dabec096ff1184b0a9b5b6ee55824d257e5112cc7e88730"
+ "digest": "32df1a577815b009696643ad80d063cc97b35d54add6d4e5517fc936f6da9ee8"
},
{
"name": "person_with_blond_hair_tone5",
"unicode": "1F471-1F3FF",
- "digest": "9e096a210ea720d32bc6a7005cd77f8b314ccf817fc3060da2e1796de39e9d60"
+ "digest": "2e270bb39187d8e36a33f4aa4d6045308189595fafc157cf7993e82d7ce93442"
},
{
"name": "person_with_pouting_face",
"unicode": "1F64E",
- "digest": "8c3199a422250d2db9a163156191ed2c6697d7f31699e2efe19e05ca26e5d225"
+ "digest": "57e9a6e5f82121516dc189173f2a63b218f726cd51014e24a18c2bdfeeec3a0b"
},
{
"name": "person_with_pouting_face_tone1",
"unicode": "1F64E-1F3FB",
- "digest": "3e1f09bbf607381c992739ea92dd35cbd26b1bbc705a7d21b7c3156f50e9d8b3"
+ "digest": "d10dadb1ac03fc2e221eff77b4c47935dc0b4fe897af3de30461e7226c3b4bbc"
},
{
"name": "person_with_pouting_face_tone2",
"unicode": "1F64E-1F3FC",
- "digest": "b5fc1cf3fdc5ff01105ee2452db90baa6a52c1e42f3795b2836c3e35197ece1f"
+ "digest": "efface531537ab934b3b96985210a2dac88de812e82e804d6ec12174e536d1cc"
},
{
"name": "person_with_pouting_face_tone3",
"unicode": "1F64E-1F3FD",
- "digest": "e8ec2539c458a8283c8c1050634c432b6363f3e64b68ba4c977994782f09b564"
+ "digest": "7ff26ece237216b949bfa96d16bd12cfd248c6fd3e4ed89aa6c735c09eafaeff"
},
{
"name": "person_with_pouting_face_tone4",
"unicode": "1F64E-1F3FE",
- "digest": "5cab7a29699decd45682583446c2bf56ddcd69cd16e14db661b526a4076dfa17"
+ "digest": "045c04105df41d94ff4942133c7394e42ff35ef76c4ccb711497ab77ae6219f2"
},
{
"name": "person_with_pouting_face_tone5",
"unicode": "1F64E-1F3FF",
- "digest": "3caebd3626fd77d849859d1c99a747f80a2b59bfa5c1854494f1ce0485539a94"
+ "digest": "783ee37f146fcf61d38af5009f5823cf6526fe99ed891979f454016bce9dd4ba"
},
{
"name": "pick",
"unicode": "26CF",
- "digest": "24a3e8f592435b97272e6d134ea5503dce3012811659c4aadbad4e45d9fba679"
+ "digest": "7f0ec5445b4d5c66cf46e2a7332946cce34bd70e9929ac7a119251a7f57f555d"
},
{
"name": "pig",
"unicode": "1F437",
- "digest": "50b55fc74e8f6c89c6e04609381c99a660748908f0ef015f5da37089678ad0c3"
+ "digest": "51362570ab36805c8f67622ee4543e38811f8abb20f732a1af2ffbff2d63d042"
},
{
"name": "pig2",
"unicode": "1F416",
- "digest": "e8189fb678608e8b9d69e11d2566f9a4765cbdff99ec8e66df30c7a2dabf742f"
+ "digest": "67010e255f28061b9d9210bcdab6edc072642ad134122a1d0c7e3a6b1795a45b"
},
{
"name": "pig_nose",
"unicode": "1F43D",
- "digest": "7e299cb49a771884f5065c68733a5a1fe354a54cff009127230177f1717af4a5"
+ "digest": "0b21cac238bf4910939fbea9bed35552378c1b605a3867d7b85c1556dbda22a9"
},
{
"name": "pill",
"unicode": "1F48A",
- "digest": "53ae3379cc6721744979122569f157a5a13aa6b48e081a89f17b2d90134efe9e"
+ "digest": "cb00be361aaba6dbcf8da58bd20b76221dd75031362ecae99496b088ed413a7f"
},
{
"name": "pineapple",
"unicode": "1F34D",
- "digest": "ceda8ffa4a41594f28a4e69d03f8a1daeb2ba20740f0b8c56447cae833eea035"
+ "digest": "621d4d4c52b59e566c2e29ed7845c8bd2d1da0946577527342097808d170dd70"
},
{
"name": "ping_pong",
"unicode": "1F3D3",
- "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39"
+ "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
},
{
"name": "table_tennis",
"unicode": "1F3D3",
- "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39"
+ "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
},
{
"name": "piracy",
@@ -8242,322 +8242,322 @@
{
"name": "pisces",
"unicode": "2653",
- "digest": "75f11b9a094196b54a242420362fa7c0aeba7cfc497b187e1aaaba96d93684a7"
+ "digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a"
},
{
"name": "pizza",
"unicode": "1F355",
- "digest": "ac94ae1c034f7b854ce2a483e1c219d101a84336f5065342f4824ff32ba705c4"
+ "digest": "169bc6c1e1d7fdab1b8bf2eab0eeec4f9a7ae08b7b9b38f33b0b0c642e72053a"
},
{
"name": "place_of_worship",
"unicode": "1F6D0",
- "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3"
+ "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
},
{
"name": "worship_symbol",
"unicode": "1F6D0",
- "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3"
+ "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
},
{
"name": "play_pause",
"unicode": "23EF",
- "digest": "d69e8cdec33447283cf65d343b986115e27681d781b721db7894e5c587ca18ad"
+ "digest": "af1498f34a3d6e0da8bbd26ebaa447e697e2df08c8eb255437cf7905c93f8c42"
},
{
"name": "point_down",
"unicode": "1F447",
- "digest": "685f46a643be7f3033896e59a822f87d61ce50db6969bcdbacc743215a96bb7a"
+ "digest": "4ecdb3f31c16dc38113b8854ec1a7884613b688a185ebdf967eab9a81018f76d"
},
{
"name": "point_down_tone1",
"unicode": "1F447-1F3FB",
- "digest": "d3dd2608fe17d5649c960fcf8dbdb68466908d80fa349b7947b457da2a27ebb1"
+ "digest": "c74a7c94367cddbfa840542dc0924adeb0d108be0c7fde8c25fb95d69115d283"
},
{
"name": "point_down_tone2",
"unicode": "1F447-1F3FC",
- "digest": "67ab236a14f6d63abcdb26433a66a183d223186c21ebc9f978fab50165ebe271"
+ "digest": "dc4bda0726d85418b974addb42738f437fbb9cf16e5815cdbab3859c4ada6cae"
},
{
"name": "point_down_tone3",
"unicode": "1F447-1F3FD",
- "digest": "c8a2368f2cedb5bbb5cc0195b97fbf3787747637bf6e77bdc9a4edf4a3f22a04"
+ "digest": "e460f81a501376d2f0ed1d45e358c5ed03ba049e8f466e4298afb4f3ca6d24dc"
},
{
"name": "point_down_tone4",
"unicode": "1F447-1F3FE",
- "digest": "6a92eab3bc8f950fa423e690f54a352887bda92f01e91c62eb3f3a9544c10cd8"
+ "digest": "4bc91cd771f24e0f897a9d8b18f323fec9a82da0fc2429c4a7e4e6a9d885a0a3"
},
{
"name": "point_down_tone5",
"unicode": "1F447-1F3FF",
- "digest": "6ad329f156414f421d6f8cf5e2a68d34b7a41f90d80e8e66b15bcbd3788126c7"
+ "digest": "7e47c6bc73250f36dc7ae1c1c09e7b41f30647b9d0ff703a53a75cc046b5057d"
},
{
"name": "point_left",
"unicode": "1F448",
- "digest": "cb520d6bba4c2b3bd7911315c9efce3261d048ff090437d7e24c9c5a7255043e"
+ "digest": "b5a7e864a0016afbadb3bec41f51ecf8c4af73cc20462e1a08b357f90bca6879"
},
{
"name": "point_left_tone1",
"unicode": "1F448-1F3FB",
- "digest": "81813901bdaa8d261277f79aff9e9a21beb80a5855899941820b25f70786ec21"
+ "digest": "9f1868272a10a2b738c065be5d30241643324550cfd47baf01c7a09060e66d31"
},
{
"name": "point_left_tone2",
"unicode": "1F448-1F3FC",
- "digest": "ecdc3dea0d644290aa7e0dab758c215822482a482ba35d825a33152453593c1e"
+ "digest": "bf0d58c68178a2c2c01d4a6235a1a66b90073cea170f9f6fe2668b6dd68424f7"
},
{
"name": "point_left_tone3",
"unicode": "1F448-1F3FD",
- "digest": "84e73b6a37755016271c255eba164f349dbd2a2badf5d9ac1c6f4cbfcae589f0"
+ "digest": "34d28c97bc8f9d111d14e328153c4298fc32cf18e39e20aacaec17846645ed90"
},
{
"name": "point_left_tone4",
"unicode": "1F448-1F3FE",
- "digest": "d16800499b6c6ede94256796b1de8a8f723879f636849856b3bd8b7a092b5576"
+ "digest": "c40c8436316915d516c53bb1c98a469528cefd98baa719be7e748c4608cbbcc9"
},
{
"name": "point_left_tone5",
"unicode": "1F448-1F3FF",
- "digest": "18b7108066cebf2d4090f29e595a2f01db94bd210f3b1d61dc269ec249a749b9"
+ "digest": "c410fe32e4ce0ded74845a54b86090e59e5820d457837b16e175b36cc71ecb46"
},
{
"name": "point_right",
"unicode": "1F449",
- "digest": "866180bf31e92de32aba336d5b5ce81773a29cdaadada1d93c944cf9ad9783bc"
+ "digest": "44d9251ab41f2f48c2250c44a47f92b3476a71f13fbbbfb637547db837fd5a49"
},
{
"name": "point_right_tone1",
"unicode": "1F449-1F3FB",
- "digest": "ebe2e4bf6bd46a5798b9a845a4ed055911c4fe58dbeacc4d39d6ea63e28e7cc9"
+ "digest": "9fcce259eb81c0b52ec7796b98a1653194e3a9021a1d338df1dbbab7522fc406"
},
{
"name": "point_right_tone2",
"unicode": "1F449-1F3FC",
- "digest": "b638662a67b1c6adde4f5abc789aae010b178404cdd1b71fcc982cdf8307c655"
+ "digest": "9d00a0b1cfc435674dc56065b3d28d28839196977504cf20581205351d8708f2"
},
{
"name": "point_right_tone3",
"unicode": "1F449-1F3FD",
- "digest": "32c6ca2f992416ab2c36672dfbc1c0de8f102c77a13496dd8d63736a7b0261d2"
+ "digest": "e3026a70630ba73d76892a055a80cac2f78d509faddce737f802d2abefa074ba"
},
{
"name": "point_right_tone4",
"unicode": "1F449-1F3FE",
- "digest": "89bd6828e9b82408a3829d49fa43332e2599f7d10bc6e5b14b750ef03267b173"
+ "digest": "ea508fde90561460361773b4e1b8e80874667b19ac115926206e7c592587cb76"
},
{
"name": "point_right_tone5",
"unicode": "1F449-1F3FF",
- "digest": "390525048a12b0efa22de550c800e439b0deaad03f1f31155d179aef093354af"
+ "digest": "d59cdb2864eb2929941ecd233f8b8afcddc30fbd4594e5f9acf6386ae06ac12c"
},
{
"name": "point_up",
"unicode": "261D",
- "digest": "31b5ca1303c1afabe1db322b24f73b23f3568c87a364f61c82f6e0c858c090e9"
+ "digest": "b69ff4f650989709f2185822d278c7773672bd9eb4a625da80f3038a2b9ce42b"
},
{
"name": "point_up_2",
"unicode": "1F446",
- "digest": "55c237054aa347c9847f3f3f577eb755db55dfcf793aa7de0f8f868574d70e8f"
+ "digest": "e83cd9eff2af5125a25f5a306c3ee3cfea240add683b5c36a86a994a8d8c805c"
},
{
"name": "point_up_2_tone1",
"unicode": "1F446-1F3FB",
- "digest": "dc07e7732d973de96ae3b08b14c19e20b6c1aea7f5a30e7198679b750422e914"
+ "digest": "b02ec3e7e04a83bfb769cffb951cbf32aa78e56fa5a51c097f9326df9e08ed33"
},
{
"name": "point_up_2_tone2",
"unicode": "1F446-1F3FC",
- "digest": "af2211fc4a1bd51d1e76f7bc43a6fa87bdd24e4295c52fdbdb01c1ca670a6cd7"
+ "digest": "32994b85c8b4a1383ca985ebc3382be88866cea1ff1315adfb71fb05e992a232"
},
{
"name": "point_up_2_tone3",
"unicode": "1F446-1F3FD",
- "digest": "917701169b3fb3e1b6e14a68e9572b25998ef2e38abac9ad8cf30100f8ea0dac"
+ "digest": "9e263bcfb82ada34ff85291f36e64e66b86760fb11a4e0c554e801644d417d6d"
},
{
"name": "point_up_2_tone4",
"unicode": "1F446-1F3FE",
- "digest": "20843904764c6c3e55792cce0c55c76f72b97788c5229cad655ebf1f2873b439"
+ "digest": "3edc92130a0851ac7b5236772ce7918d088689221df287098688e1ed5b3ff181"
},
{
"name": "point_up_2_tone5",
"unicode": "1F446-1F3FF",
- "digest": "1d0cca546027c717da50f90da65757af46fe7cd4e397da9b8e203446f707208d"
+ "digest": "cabb3b7da9290840ef59d0c8b22625bdb2e94842f01b0a575ccbc348f3069d77"
},
{
"name": "point_up_tone1",
"unicode": "261D-1F3FB",
- "digest": "5ede60379dee23166c6b834d73da8b55268e330f67058843b8a3705dca6ed71a"
+ "digest": "e496fda349072f8b321ceb7a251175f7244c3076661f5ede48ea75ba1acf8339"
},
{
"name": "point_up_tone2",
"unicode": "261D-1F3FC",
- "digest": "c94a15ef848d410aa5d32b8d0e453b59682fde6f39e6705cbb81cf0829833a81"
+ "digest": "5a8081323f3baa67e6431e21e16a36559b339f5175d586644e34947f738dd07a"
},
{
"name": "point_up_tone3",
"unicode": "261D-1F3FD",
- "digest": "d319ce72876d97a3b1d4bc7c0679e546a983f02145d723a0da5ed0b73a51cfe7"
+ "digest": "07bf0cea812eb226b443334e026e13d1ec23e013478f4af862a3919703107842"
},
{
"name": "point_up_tone4",
"unicode": "261D-1F3FE",
- "digest": "9171a27f86f27fd144347a17153fb56e30bd32e67a8f10f8c1f32a40cad4e009"
+ "digest": "1fbbd71433108143ee157d0fdadd183f7f013bafa96f0dd93b181e1fd5fd4af2"
},
{
"name": "point_up_tone5",
"unicode": "261D-1F3FF",
- "digest": "a894f87da4c3d33d5e6e74d003a33ec60c453db6507fe05d22235f807ead27d6"
+ "digest": "ad068ef32df32f8297955490a9a90590a0f93ed5702a052cd0d8f6484c6cc679"
},
{
"name": "police_car",
"unicode": "1F693",
- "digest": "7999869cb75be404fc34942b6f9d8e84fa7e259aa892a1e8e1652a5f02cceea6"
+ "digest": "0909be1bd615ae331a7cce71e16dee3ca663c721d5170072c593cb7c76f9f661"
},
{
"name": "poodle",
"unicode": "1F429",
- "digest": "8a568d8688bf19b440b7c1b49fcfe6672b8f75af0031d89ab6212623430acadb"
+ "digest": "f1742fdf3fd26a8a5cfeaba57026518dacaad364cbd03344c4000a35af13e47a"
},
{
"name": "poop",
"unicode": "1F4A9",
- "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
},
{
"name": "shit",
"unicode": "1F4A9",
- "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
},
{
"name": "hankey",
"unicode": "1F4A9",
- "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
},
{
"name": "poo",
"unicode": "1F4A9",
- "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
},
{
"name": "popcorn",
"unicode": "1F37F",
- "digest": "12264cb16fca9317e3ba8d5924a2c8f15f790e36d2f29e7b12aaaf77e1beb73d"
+ "digest": "684f1b7ef34ea7ca933aed41569bc6595a19ef0d546a1b7b9e69f8335540b323"
},
{
"name": "post_office",
"unicode": "1F3E3",
- "digest": "5e2d896cd646a2eecd5596af9e44ca1fa2745de5cedaf0f6d193b8243201c6cc"
+ "digest": "54398ee396c1314a7993b1cb1cba264946b5c9d5a7dbb43fd67286854d1d1a0f"
},
{
"name": "postal_horn",
"unicode": "1F4EF",
- "digest": "339aa61fa1567a1d159bb8204d15db889fbb6cc1106f6e1991b4a184d1bc1fc7"
+ "digest": "0ea12f44f3bae9a14bde3b37361b48bd738d2f613bb1b53a9204959b70e643f8"
},
{
"name": "postbox",
"unicode": "1F4EE",
- "digest": "ef1a6543fccb9f1009cc3782c51883e51167721a0b49e8ba21e8e6049b216906"
+ "digest": "bbc424ae8d46de380d7023a43ea064002fd614657d00330d3503275827ac87e2"
},
{
"name": "potable_water",
"unicode": "1F6B0",
- "digest": "4a2379835660dfa8b6780d662a10d1effab710f471eb9b5e6ade4772ba7e5aeb"
+ "digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098"
},
{
"name": "pouch",
"unicode": "1F45D",
- "digest": "cbd47ec1a65f5c642773d8ea2e7e57f7041a2d7ed9df05fbdd7bc8743c6dece6"
+ "digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351"
},
{
"name": "poultry_leg",
"unicode": "1F357",
- "digest": "d416e9464bd58073bd3e32eb06c0da96905609f47b9d667acdc0810e94237584"
+ "digest": "1445ec4f5e68a19e5a84e5537dca8190d62409070c954d112e6097f1a6b7f054"
},
{
"name": "pound",
"unicode": "1F4B7",
- "digest": "1ac491bb8a91613b2b1faaac4e7b4bc794d2abef69ac79de17d54c824c3ef826"
+ "digest": "eb11b83eb52adb0a15e69a3bc15788a2dc7825dedee81ac3af84963c9dd517b5"
},
{
"name": "pouting_cat",
"unicode": "1F63E",
- "digest": "ba28d75401d5bb98773acd35aaf173356bae4d5a5520a226559478138364ebdf"
+ "digest": "8822abedf3499cf98278d7eeea0764d1100ec25cad71b4b2e877f9346f8c8138"
},
{
"name": "pray",
"unicode": "1F64F",
- "digest": "fb0df9c1566014bd2df2a1afd59366b896f20c03ca3516e02e4be44ea556c8ea"
+ "digest": "735b79dab34ac2cf81fd42fdcd7eb1f13c24655e5e343816d5764896c03edeea"
},
{
"name": "pray_tone1",
"unicode": "1F64F-1F3FB",
- "digest": "c6d8cb46e65ad13a92e85f97e018176fd89513f23e899e15d1ad09e3b4009f4b"
+ "digest": "e8b6103450215e8566797f150978355e297deade4eb47a6371f7a7bc558fed9d"
},
{
"name": "pray_tone2",
"unicode": "1F64F-1F3FC",
- "digest": "2cd68cbe1ba3254f173ec8136af79cae64873bd0f20480158c3e6babd5a1a442"
+ "digest": "ee8baacd95d7e8dbad8a1f2d9a12e36c98f3d518db5d3b117d0a18290815e62b"
},
{
"name": "pray_tone3",
"unicode": "1F64F-1F3FD",
- "digest": "d2e81863f74a87b96335fb108e7b206f28ed18185362ab4d42a3b0523801398b"
+ "digest": "ae8c0caa9aca0a6c44069e76a7535c961d0284cd701812f76bbd2bd79ce2bd53"
},
{
"name": "pray_tone4",
"unicode": "1F64F-1F3FE",
- "digest": "ad1b91254b101d872325c325ebd1f2a6257cfe22e83de88e29dd16ffac191979"
+ "digest": "64f7b3178b8cd6f6a877ed583539eefe068fa87a0dd658fdcd58c8bc809f7e17"
},
{
"name": "pray_tone5",
"unicode": "1F64F-1F3FF",
- "digest": "23f40a11321decbdc6a1d274b9ad571041d261d364d13d1063c306e73ad52254"
+ "digest": "5bc8cdce937ac06779c87021423efcec4f602aa4a39dba90b00de81033005332"
},
{
"name": "prayer_beads",
"unicode": "1F4FF",
- "digest": "cb6f8700154f75749cf2642a25c03e255dc18428baf8b57f6bd807c92b83e28d"
+ "digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05"
},
{
"name": "princess",
"unicode": "1F478",
- "digest": "47b93eb52d757c3c000d9760391ecb942776d883b28050d833fa11612483d8ee"
+ "digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80"
},
{
"name": "princess_tone1",
"unicode": "1F478-1F3FB",
- "digest": "1e4073c2abdf51a61a1a85a3e063541fe96e9b9ec36ec6f7fb9c98deeb230869"
+ "digest": "52b88b99ba64f82e8f36e2a1827c85145e4fcd6863478c2345fe9fa9e8901cdf"
},
{
"name": "princess_tone2",
"unicode": "1F478-1F3FC",
- "digest": "6a0a5dc447cd887798f908c15972e7a12d28d81f168b92bcb105786ac253bea0"
+ "digest": "7e44289404693668f20e681fcdc2e516512d54a69c627eedae958f69dfe6eea9"
},
{
"name": "princess_tone3",
"unicode": "1F478-1F3FD",
- "digest": "2f08d22fdfc7a7d66fcd87ae716b811f43077f5bb17fef87f5b7e2aa93700d70"
+ "digest": "96c9a9857348d7a1a8be899c50d55b352b9a9fd5c65e4777bfa199fe7929d41c"
},
{
"name": "princess_tone4",
"unicode": "1F478-1F3FE",
- "digest": "02129211bf7bf7ff6de35913b7069aee151532d878b8c4f7e24c012e5b09d4b4"
+ "digest": "67696f96be60f2a36598072172d2db197d007e6c1ac3acef526a5ce6d59bf3f7"
},
{
"name": "princess_tone5",
"unicode": "1F478-1F3FF",
- "digest": "d676f103600b69dbfdb469469a77b9d561ec460ff862befa58ab30ddc909c9f7"
+ "digest": "007f624e2fad91bb57ce32ecd35213a796d71807f3b12f3f1575bf50e6a50eeb"
},
{
"name": "printer",
"unicode": "1F5A8",
- "digest": "c44402c87071f8d31d3997abab53ab9f8f7c11434e747380928814ceb6b0a417"
+ "digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8"
},
{
"name": "prohibited",
@@ -8572,57 +8572,57 @@
{
"name": "projector",
"unicode": "1F4FD",
- "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e"
+ "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
},
{
"name": "film_projector",
"unicode": "1F4FD",
- "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e"
+ "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
},
{
"name": "punch",
"unicode": "1F44A",
- "digest": "5759db1d7093744c74b840bbb4761fb025d6633f8fa539bcb35dcf54fc05ceb6"
+ "digest": "c7e7edf6d64f755db3f02874354f08337b3971aff329476d19ac946e0b421329"
},
{
"name": "punch_tone1",
"unicode": "1F44A-1F3FB",
- "digest": "793b3fa2a43c23b2c1e1b48b86ae35e8c4024cd065fac0a0a5ada87cb78d6de3"
+ "digest": "c9ba508b0c36041047473782acfedab5af40dd7946b33daf4d8d54c726e06a11"
},
{
"name": "punch_tone2",
"unicode": "1F44A-1F3FC",
- "digest": "6fc2467e99982ab00b0c352c6f7793d34faf17b16a0312082c9bd1f0709e3938"
+ "digest": "d53011cd2f3334c7b3fffdfe1e2b8cc1c832c74306e1ac6d03f954a1309d7d0b"
},
{
"name": "punch_tone3",
"unicode": "1F44A-1F3FD",
- "digest": "bf747b29952550c5b4d3807b9ed85b5e5d4bcc3265b0e214791f7db547f861fb"
+ "digest": "f7522347094e0130ed8e304678106574dbd7dd2b6b3aeb4d8a7a0fef880920b2"
},
{
"name": "punch_tone4",
"unicode": "1F44A-1F3FE",
- "digest": "3b6c0ccb682552f32d6744c438e3af04a1732c67a74bcafb14c723cf526fed87"
+ "digest": "3e62bdd426f3e6ff175ce3b8dd6f6d3998d9c1506128defa96b528b455295b47"
},
{
"name": "punch_tone5",
"unicode": "1F44A-1F3FF",
- "digest": "945bae1aa3587cd1dc57d1ec4da18c67a59e0e7150dcc8735e5357b4ea1234ac"
+ "digest": "7d9bff777dc4ec41ac132b1252fa08cf92a398c8dc146c4a5327b45d568982d8"
},
{
"name": "purple_heart",
"unicode": "1F49C",
- "digest": "e0eb886e74f22d40d059ff3a089d472af53c6c53de380f428cca140dfd046345"
+ "digest": "a6bf01de806525942be480e45a4b2879f91df8129b78a1b8734d4f917bcab773"
},
{
"name": "purse",
"unicode": "1F45B",
- "digest": "67d82ff9a4d76148b9d98538d4b786f880058a556e650ec3f93e1632aa42aaa7"
+ "digest": "2b785f36e01875d66cfda2192c8c53606e7224a7c869a4826b62cb61613d60c8"
},
{
"name": "pushpin",
"unicode": "1F4CC",
- "digest": "c4de129d5d8744caffeb2f499fcc0bc6b551843938f8166ffecd0de00bda66e3"
+ "digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8"
},
{
"name": "pushpin_black",
@@ -8632,202 +8632,202 @@
{
"name": "put_litter_in_its_place",
"unicode": "1F6AE",
- "digest": "b26d3b68bd62d30ecfe75cfaf309a7a0f91e92db0aa18b0b97b97baf0609d4e6"
+ "digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c"
},
{
"name": "question",
"unicode": "2753",
- "digest": "258e3169bae177fb0f01ed5f9b933f7f02dd2673e12a316af44a0c3729a78a2c"
+ "digest": "40050a1fd29bed321fd601d13dc33de5d6084121f1d873b29bde9dc3d823a310"
},
{
"name": "rabbit",
"unicode": "1F430",
- "digest": "9817a7454aeda77d28f63eb13c0dc0a6d9e6c9abe3dcf538b4b3477e494cddb6"
+ "digest": "678ad953a7ab8f618c59051449a67c965d1f04f42dd6f6669adaf3fadebd080c"
},
{
"name": "rabbit2",
"unicode": "1F407",
- "digest": "67ba57a31b0768a2118faabdcb088f96f1441e1132397f65b6937d523ff7dabb"
+ "digest": "19b1f5108292472434cc7a49efac4ea9275779735c7aeb0f15c36021d5998ca0"
},
{
"name": "race_car",
"unicode": "1F3CE",
- "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c"
+ "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
},
{
"name": "racing_car",
"unicode": "1F3CE",
- "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c"
+ "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
},
{
"name": "racehorse",
"unicode": "1F40E",
- "digest": "36aa3c7123ee7e15600657166032b21b8edeb192cf6d3ada39b5c65001f7fc40"
+ "digest": "a57b7aca35347ada8225eeee06b70cfd040484104963b4df56ea8fec690576b0"
},
{
"name": "radio",
"unicode": "1F4FB",
- "digest": "b1403f9a883405b909208f52c9474c2d3923681ea0b02609a6e9dc12460319a5"
+ "digest": "9245951dd779cdd141089891b15a90d3999a6358acf1fc296aa505100f812108"
},
{
"name": "radio_button",
"unicode": "1F518",
- "digest": "9bcdac17b3620331a32f9bb876812231a701eb5a7f696e7d875f877ab92159fc"
+ "digest": "565bec59198df2592e96564c6e314d3cde33c47b453db1bec6c5d027b5cb4fd9"
},
{
"name": "radioactive",
"unicode": "2622",
- "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4"
+ "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
},
{
"name": "radioactive_sign",
"unicode": "2622",
- "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4"
+ "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
},
{
"name": "rage",
"unicode": "1F621",
- "digest": "02ac70551fc51478884c133b29539cae58b463c760db38c0aeec1bdf5b282312"
+ "digest": "d97ba6bd08eec46dbc7199f530c945b73a87a878e35397b0a3e4f2b45039e89e"
},
{
"name": "railway_car",
"unicode": "1F683",
- "digest": "8490e2ecf94c7c1d1e22fea0d80cc18a49648741009e51984f583b17bbd022e2"
+ "digest": "2cddc08d555e7fc24e312c3d255ed013fbf9cd2974a6918369c32554049ba2be"
},
{
"name": "railway_track",
"unicode": "1F6E4",
- "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e"
+ "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
},
{
"name": "railroad_track",
"unicode": "1F6E4",
- "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e"
+ "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
},
{
"name": "rainbow",
"unicode": "1F308",
- "digest": "bbd8ecc8d0737948969a3539d2d202e599404e509f1a21bdbb0a0c41c2540522"
+ "digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d"
},
{
"name": "raised_hand",
"unicode": "270B",
- "digest": "4192881a0d613b4fcb19b1c2d8b83aadee6f0b12170721c8dd7b1ccef6540199"
+ "digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a"
},
{
"name": "raised_hand_tone1",
"unicode": "270B-1F3FB",
- "digest": "df2e046c99dceb9184c50a777b403d72bfb25ff473d6a4e20bb9a731db64ed8d"
+ "digest": "865afca29b57577fed8fe8c2be57b74254a008c8cf34194680be2759239b5f5d"
},
{
"name": "raised_hand_tone2",
"unicode": "270B-1F3FC",
- "digest": "ed179299a1c397cd51cf6067d6795d71a3831d35e1ec9eacbf0286c8992c1e7a"
+ "digest": "832169a0b626a682a58a3b998f68413657b4962c1fab05f1fdc2668e82727210"
},
{
"name": "raised_hand_tone3",
"unicode": "270B-1F3FD",
- "digest": "cacbd0ddef65bc01a41bd921ea159f8cd89050309b10f15780d6199f79434a54"
+ "digest": "3959a873ad7671de82c615c4ed840b011e67baafb2bab7dd16859608d3e83cb1"
},
{
"name": "raised_hand_tone4",
"unicode": "270B-1F3FE",
- "digest": "04c934c7a55b83bcfa7f3880fc1f6aa0f188090c37b9670e6775a512a1cf59e9"
+ "digest": "db542f65d076ccf3dbfca27cb7c2f135a8bf7a487a81a04873e70172bdfcd579"
},
{
"name": "raised_hand_tone5",
"unicode": "270B-1F3FF",
- "digest": "da0c4283b7b19861237c023234c6db28045b8f5a5971acb015733e08e2940e86"
+ "digest": "88ca884d14baaae48df21d75c22d82fb15bdc395e42026f5ca34cd65e5ae8674"
},
{
"name": "raised_hands",
"unicode": "1F64C",
- "digest": "308e475f38558e73bd66e28693d77478caa5bca4360cffaffc2a97b5858c56ba"
+ "digest": "2ee73466a3f5079e542857fe6f5497e9f87753a81854985ce3356a8d3da1d8b8"
},
{
"name": "raised_hands_tone1",
"unicode": "1F64C-1F3FB",
- "digest": "e39b9bc49dccc127e44f543e98961fcf5bcd44d6e216741bcd10ec3667263c84"
+ "digest": "43e73c60f040a66374b8ec98f3629a90d13ae9f472446ed7676cd5573e824f4b"
},
{
"name": "raised_hands_tone2",
"unicode": "1F64C-1F3FC",
- "digest": "f376ab13071ffdc11888ec221ef5b4de546ca0f60bd9ae30bf3da4066c220462"
+ "digest": "fcc5255bb2b06dc82d6878e74cf34e8ce118c70004a06d39a980683772b98c52"
},
{
"name": "raised_hands_tone3",
"unicode": "1F64C-1F3FD",
- "digest": "67694325a43e629c00fa9bd2ff7e19f84f216b2855ae2cf097762dfa7aca25e6"
+ "digest": "3ee3e0aafef486e766a166935e8147fb75a7329cfebc96dec876cc45e83a8754"
},
{
"name": "raised_hands_tone4",
"unicode": "1F64C-1F3FE",
- "digest": "a2254fe75a0770708916a4ddd5db4420221c6ea9db9f74068d14eadfc0f3772c"
+ "digest": "78a8cbf6b2b85be4d6b18f0ff6a77f197963117955725fb7e57e0441effb928f"
},
{
"name": "raised_hands_tone5",
"unicode": "1F64C-1F3FF",
- "digest": "bd7c9897cefb454ccdc46027bf56d6587565bdd345d7d0f081b7b671a53f6c99"
+ "digest": "2a5ed7334a17172db0cd820a559e7f75df40ec44de6c25d194c76e1b58c634cb"
},
{
"name": "raising_hand",
"unicode": "1F64B",
- "digest": "d57178fc77e9fa140682634da35f9ab12a65d9b4c506b7cd8a9697f1b5910bdb"
+ "digest": "512750b00704f1ccefd3c757743540b785ad7670dbbe4a2c4dca8d93e6701920"
},
{
"name": "raising_hand_tone1",
"unicode": "1F64B-1F3FB",
- "digest": "f46b34361ef79743f3187d6860182bbe1ae411031db7fe5c0f7292fa472b9c16"
+ "digest": "2897722f091c273dd3714cff7423c2475bc3070416c28014ca03322b9ece48bc"
},
{
"name": "raising_hand_tone2",
"unicode": "1F64B-1F3FC",
- "digest": "20b85a2ebca150b2020a04b41d34884c78c22f42c251e2b9d23fd3724574143b"
+ "digest": "59199b334b3845911382c1f29bd7c0d5ef9d2486417345e265b166ead7d3e1c1"
},
{
"name": "raising_hand_tone3",
"unicode": "1F64B-1F3FD",
- "digest": "5e0401b528c2b8edff766d39cdcedbe9abebe4c940df7a36ace61f59c08d508a"
+ "digest": "f95b338d5efcf14ef12f415a2c1bba93df48628ddc94f34f70c31e1b3c2e1d28"
},
{
"name": "raising_hand_tone4",
"unicode": "1F64B-1F3FE",
- "digest": "e4f5624264269ad09cde207cd7d4eb0fd46de816880daeec457ac8cd51cc1b7b"
+ "digest": "951ddbfdb57d5a60551b59b3d0f7ca00a64912f4a101a73afaebd68445cd6cec"
},
{
"name": "raising_hand_tone5",
"unicode": "1F64B-1F3FF",
- "digest": "eb34b6c037bee5bbc4222f6aab421aa785f527ebf1b5e971769e5102244d60e1"
+ "digest": "9370f93704d8f89ca6dc946715eab5e7dba82bf04dd68c00f5c0abb8bc16371e"
},
{
"name": "ram",
"unicode": "1F40F",
- "digest": "b71950d7a286a4c4909c5ec7c35211c2a5c20b6bad341bd863c6a85c4bcf9c80"
+ "digest": "2875ab28e1018b39062aeb0c5ce488c48a98f13e9f2364470a0a700b126604f2"
},
{
"name": "ramen",
"unicode": "1F35C",
- "digest": "7dd185b24852b577913edc78647cd53b27d42e225fde29aa2f3aba25c980b5c4"
+ "digest": "425662a49c4c13577c0de8d45d004e5ba204aaadbaabae62a5c283ecd7a9a2c5"
},
{
"name": "rat",
"unicode": "1F400",
- "digest": "7a10d9ba5ee1010d421d9cf73d7966507302a69617a32fe9f1a00d57a31f7bd7"
+ "digest": "14380d65498c6ce037c02a93bca2b24f25a368d85278d6015b8c9f7cd261f8e2"
},
{
"name": "record_button",
"unicode": "23FA",
- "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
+ "digest": "92be12161ba206bb2e06a39131711c7b17368d55b4aae0b48f0ac5b6b1cde76b"
},
{
"name": "recycle",
"unicode": "267B",
- "digest": "74a54ed62a40dfbdcace1f08b085658a77d45c62570273927ad270bf9a8a2f4d"
+ "digest": "c377e8537367b05b5de9be860a0fcabd7aed2bf4ba146eefc423671a21530369"
},
{
"name": "red_car",
"unicode": "1F697",
- "digest": "558730d6418aa5d85b73af58c8041efd12cff906e26ea47c50963f66d33d6eb8"
+ "digest": "8a99832a195263c0e922af53d52dea37aa3e07032b3c2a1977f8527b4a144b9c"
},
{
"name": "red_circle",
@@ -8837,72 +8837,72 @@
{
"name": "registered",
"unicode": "00AE",
- "digest": "ed924107384461aabb4924c401c6c087ffa047bc2ef735823e7c2be67804707c"
+ "digest": "9661b1df529ecb752d130820c55c403e5de263748eb02f7fea327818bc282d94"
},
{
"name": "relaxed",
"unicode": "263A",
- "digest": "65072f7b9bfaaa92b8a0ed012dffe2cfd2efa3748264aaf450aa31ba6bd44045"
+ "digest": "2d5aed4fb8504c6d6660ef8d3bfe0cc053dcd6099c2f53748c202dc970c639bc"
},
{
"name": "relieved",
"unicode": "1F60C",
- "digest": "1f2c7ae6a9d74a112de89403be6eca3d8155d70395e7fce51032fc961f235c7d"
+ "digest": "b4ce2ba6c220d887fe5e333c05ed773df9b6df0ac456879fd8f5103ff68604a5"
},
{
"name": "reminder_ribbon",
"unicode": "1F397",
- "digest": "e4a2afc7dce40589657f7043ba8acc9638fd4117252278233ea89f84cddad387"
+ "digest": "c3de2a7c9350b77a0b86c0dcce9dcd9953ea8a97aa1e7aed149755924742f54d"
},
{
"name": "repeat",
"unicode": "1F501",
- "digest": "27b6dad9215e58e24c607a39dbf398ecf66ccb692c81e08eb2f5f4912db30522"
+ "digest": "b9512d508613ed0eb3181eb1030f7f6fd6b994476ecdfa308733c6df975fb99e"
},
{
"name": "repeat_one",
"unicode": "1F502",
- "digest": "052d13f2b08eaf70b31252aa78f95d06fbe22c58945c19381b13cbeb1c855651"
+ "digest": "53409cf24dd4bb0d7b50ae359f15d06b87b7f4a292ed5c3a09652fa421a90bf2"
},
{
"name": "restroom",
"unicode": "1F6BB",
- "digest": "b77fbc4247c241362e5ef9e6eb58b1b437aa9d16b65886cec0c55ceb55c1440e"
+ "digest": "2e7a1bfc9a9d49b0272230a91db7369e24d54bf1de8e683d36b85f1d8c037f77"
},
{
"name": "revolving_hearts",
"unicode": "1F49E",
- "digest": "2b8925d3e78df2dba8534252fe60bf03285346f6b3697be7668bd568e6d85931"
+ "digest": "c43d3197cb4cf06659f643638f6c4e91a2889e0f6531b7d81ea826c2a8b784fc"
},
{
"name": "rewind",
"unicode": "23EA",
- "digest": "91a95b26d12ca76111556096f4d96484c9f1d7e1b20ccff5a3291b36e529a6d1"
+ "digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd"
},
{
"name": "ribbon",
"unicode": "1F380",
- "digest": "9c0296d8c2baa84c99347c431bf79b288d98b5f17b1ce7605ad7ce1da265d5aa"
+ "digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828"
},
{
"name": "rice",
"unicode": "1F35A",
- "digest": "e34849496a79e71ae4700df94f2a54895bf6de758a92edeae33fe78295a3ba21"
+ "digest": "f544f12606de59d28739798003f14ebd8869856add8e24496ec5dda3e131daf4"
},
{
"name": "rice_ball",
"unicode": "1F359",
- "digest": "52df5da8b0edbdeb56d66e0f30ad4549abdd81c064f7269d920dcac66a3df2e4"
+ "digest": "2cba6f5364cd366859bc8948897b65fc97b225ea7973d9be3b24aba388fed8e8"
},
{
"name": "rice_cracker",
"unicode": "1F358",
- "digest": "d55f8f9d807f4619eb243c510938067a7417a64bd9435b05dfeb2a36fdb2b6a0"
+ "digest": "ac0f805d41d4f322154c1968bd3ce3e9aabcd39d908182e52fd7d28458dbef92"
},
{
"name": "rice_scene",
"unicode": "1F391",
- "digest": "482d854d8d30edfc1ecd48a4ce476e6498606321405bf5a0b4ff74489a092af8"
+ "digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5"
},
{
"name": "right_speaker",
@@ -8932,7 +8932,7 @@
{
"name": "ring",
"unicode": "1F48D",
- "digest": "ae2a93e7895b9b89f5a39f01d356ffed988f219ef8b658a56c55285826a4533b"
+ "digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d"
},
{
"name": "ringing_bell",
@@ -8942,47 +8942,47 @@
{
"name": "robot",
"unicode": "1F916",
- "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60"
+ "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
},
{
"name": "robot_face",
"unicode": "1F916",
- "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60"
+ "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
},
{
"name": "rocket",
"unicode": "1F680",
- "digest": "65d8bd005ceac41904237b7a8c5f55f16713a55d971522f0bbe63a1d548e515d"
+ "digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d"
},
{
"name": "roller_coaster",
"unicode": "1F3A2",
- "digest": "907baab1f3d7becf3f8a3b1264642b395bd73b4af49e23058b3abb5c69e9106a"
+ "digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c"
},
{
"name": "rolling_eyes",
"unicode": "1F644",
- "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe"
+ "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
},
{
"name": "face_with_rolling_eyes",
"unicode": "1F644",
- "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe"
+ "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
},
{
"name": "rooster",
"unicode": "1F413",
- "digest": "6cefdaa45631ed8c9480e15f578c793d95af81b42687164fd7900eee325ccf07"
+ "digest": "2b90c5cf6fa46da13eb77285443d600afcea0c48bd1d215d60167e7dc510da5d"
},
{
"name": "rose",
"unicode": "1F339",
- "digest": "584909a4a2ece625c688f8479a39692bb8e816b692e6eb7dfd40cb045259b1b2"
+ "digest": "73799e459dba188de4de704605d824242feeb65d587c5bf9109acf528d037146"
},
{
"name": "rosette",
"unicode": "1F3F5",
- "digest": "0ce3b85ca05124ab99d57ebc9aa17bb246ee614d2fcda1ef62bf42ac7e616148"
+ "digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5"
},
{
"name": "rosette_black",
@@ -8992,542 +8992,542 @@
{
"name": "rotating_light",
"unicode": "1F6A8",
- "digest": "369e069e0bfecc7413e75f4015e9c1de527a33c7cce3f6c2b4adb60a0d9d338c"
+ "digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f"
},
{
"name": "round_pushpin",
"unicode": "1F4CD",
- "digest": "1bc5fe5a90a6e56ea00246f1b008a0e0cce0d77c226dc0300bf9a2804b543877"
+ "digest": "8ffca77bbdc6f1f726daf3abd6eff338a5ad1aa9b09dbbd8782c1e7ef5452f30"
},
{
"name": "rowboat",
"unicode": "1F6A3",
- "digest": "c10e09bf8be8b1a8ef3113edd9327126d6a4644f3bc81c7ada2922851e4d1cfb"
+ "digest": "83715d83a061926d4ad3bb569b21f5d337e3ebd4c9bcdfe493e661c12adc0a16"
},
{
"name": "rowboat_tone1",
"unicode": "1F6A3-1F3FB",
- "digest": "a84fc1b30d1a284dcd3899dc4de8f11e7b65c258528eb41c7dbf8f82425fee12"
+ "digest": "e279ac816442c0876fba1f42c700b80f2fb6de671e1a8a9e9d11b71eed5c58e8"
},
{
"name": "rowboat_tone2",
"unicode": "1F6A3-1F3FC",
- "digest": "85f001430a2ad607a15901f7c2dcf8381471f42d6cc0775e76a2ff1f457151c1"
+ "digest": "6a48eba352ed4971d26498b6c622e5772389c89c5205ed02acde8e995dddcc3b"
},
{
"name": "rowboat_tone3",
"unicode": "1F6A3-1F3FD",
- "digest": "adf8b1e45a46a13f3db40c29df0312216558e9d0c615aa46a8e913cee5003a81"
+ "digest": "875948f6d8354ebd95ce9a66fde30f06a8366dcd89d5ca3e660845f8801e9305"
},
{
"name": "rowboat_tone4",
"unicode": "1F6A3-1F3FE",
- "digest": "05482749ec40bdf02e53fc42d316c51f4f3ed643f21e8fc16b81930e4a884bda"
+ "digest": "8c7ac7346b0020d0ff5e2f4a1efb1b7785eac637f17556663ec33e2335083f0a"
},
{
"name": "rowboat_tone5",
"unicode": "1F6A3-1F3FF",
- "digest": "d4bb337d948996d4a23d87f99988f02fc207815b862082ffd2eef5f0c1016aa9"
+ "digest": "a399dbb647892b22323e0bf17bc36a9b5f1708ebedf9ba525233ee7b9d48339a"
},
{
"name": "rugby_football",
"unicode": "1F3C9",
- "digest": "e14aebbded78d4a5e9b4028f79a8ca840d02798c6758cb9e926e992e2a35a4f3"
+ "digest": "cc6f00ade3e0bbb7899e7bfb138b57216dd66de26d7967d5ffa501f382ed09f4"
},
{
"name": "runner",
"unicode": "1F3C3",
- "digest": "58a884f06d37b0ce78197bebcd3f0e102dd90022ebd86ec70a2ef5a5cdf9683b"
+ "digest": "e9af7b591be60ade2049dbada0f062ba2d3e17f02bec76cbd34ce68854a2a10c"
},
{
"name": "runner_tone1",
"unicode": "1F3C3-1F3FB",
- "digest": "65f1633d1517803de23686d2dbcc75a5787874266db4981138ccdbe4badc773c"
+ "digest": "21091cbb09c558712ecf63548bf28b7995df42bdb85235088799a517800e52f5"
},
{
"name": "runner_tone2",
"unicode": "1F3C3-1F3FC",
- "digest": "2bc81f3fb77445cdc75c34806ab0ce912bacfe47f63b5d2011a4f5d370cf7064"
+ "digest": "1fe3d194f675a46fe67799394192e66c407dd81163363692c5e7da32ddb9af2b"
},
{
"name": "runner_tone3",
"unicode": "1F3C3-1F3FD",
- "digest": "beaf5f254cba2991fdd0c38ce2ddd1b4c1110e15b2b7bc026d32f162e295c4ef"
+ "digest": "8cea1bf4ef3be71f42dc5bae978d5b7a197a3851543225349ef0dda29a370537"
},
{
"name": "runner_tone4",
"unicode": "1F3C3-1F3FE",
- "digest": "21d531ba9b3d13747ad636b8f7a6f184c974bf61d9f529975a64f9629263c407"
+ "digest": "c33f0b8b5a71d295fb6ba322e79446964a8eca9e4573efd591e4273808b088a0"
},
{
"name": "runner_tone5",
"unicode": "1F3C3-1F3FF",
- "digest": "b02a5bcc58cc45f8219262ec44c77764172fd8f2624d9122ded4a5a5db04c0ed"
+ "digest": "9f59e6dd0fdf2f17bceb41f5c355b4e6f3c8bb8cbd8af0992f0b5630ff8892e8"
},
{
"name": "running_shirt_with_sash",
"unicode": "1F3BD",
- "digest": "431bed35f4a55175bf99af769e74a81e8650c6ab34af6ecddaa1417ff7e437e6"
+ "digest": "7542307d3595aca45e8ccae66b6e58b6e92870144b738263d5379ec6dc992b76"
},
{
"name": "sa",
"unicode": "1F202",
- "digest": "a47a480631f874e8a2cd69b5d513f90a1e81a96bfa2f6025bf244a82baca3656"
+ "digest": "6042bcabd1516ef3847d695aba22851c49421244432d256e24eba04e8a223dab"
},
{
"name": "sagittarius",
"unicode": "2650",
- "digest": "14871e6681c35e4a63a0b19613f77b3674d00cb78d06975e02ca29e61b5cea8c"
+ "digest": "a02593e025023f2e82a01c587a8c0bbb1eff88cbcabf535a1558413eb32ed1d5"
},
{
"name": "sailboat",
"unicode": "26F5",
- "digest": "6f742dde6c180a174b771aa3942b558e98a3dc1eb212dd31add86c5fa5620865"
+ "digest": "c95ef4dc939cbdcb757ef3cd90331310e8c0a426add8cc800bae2540148a3195"
},
{
"name": "sake",
"unicode": "1F376",
- "digest": "aa1392790c805950779dde7778292c937f8c1aaecb522876171d5ee542ec51f8"
+ "digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4"
},
{
"name": "sandal",
"unicode": "1F461",
- "digest": "14f1e9003a6acd90a55f23c48ed87a758fca586f2e0b0edc4dc9d1deef9eb067"
+ "digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d"
},
{
"name": "santa",
"unicode": "1F385",
- "digest": "12feddd84eb49ce30ae68d4f93d66e2c0dd11297a4d1275c9a50d4f35bea83a9"
+ "digest": "178513e3d815917e59958870f5885b3414b43a16b8056980c863a468dfe00179"
},
{
"name": "santa_tone1",
"unicode": "1F385-1F3FB",
- "digest": "a75813770efe27d5b4c80ad892d0c796d88d1a0dbb1bd02d5f68882d7abad479"
+ "digest": "bf900bbc19bbd329229add9326e28e8197b69d6ddceb69f42162b0200fde5d16"
},
{
"name": "santa_tone2",
"unicode": "1F385-1F3FC",
- "digest": "90f8072fdde5f4a275cbd1902d6c94689d453b1bee0336213dc9d6f7e1d038e1"
+ "digest": "7340f2171adab97198e3eecac8b0d84c4c2a41f84606301a0d10e9fe655c93d1"
},
{
"name": "santa_tone3",
"unicode": "1F385-1F3FD",
- "digest": "0973053e7b77d268080126a50b95b45429630e5d49f62210e7b71840794c7dc5"
+ "digest": "7368ab75454ec28d8f7d6baef6ad69b5278445a9f50753f6624731bffde32054"
},
{
"name": "santa_tone4",
"unicode": "1F385-1F3FE",
- "digest": "5cd49c0d199a42846b400b3c1244d448ed6fe5ce993d379817cb2a5f7c0b609b"
+ "digest": "0ee60188353e0ee7772079c192bebbc6d49e74e63906f840c66da4eb35f4f245"
},
{
"name": "santa_tone5",
"unicode": "1F385-1F3FF",
- "digest": "a54c36dfa99b39549fb1d3dd7f0021a7aee28112960172ed466dacc67961c525"
+ "digest": "e4378a0cc5d21e9b9fe6e35c32d1ebc6fb8c2e1c09554cd096aeaefd3a6eb511"
},
{
"name": "satellite",
"unicode": "1F4E1",
- "digest": "3b9797c8161526edce0bd8e9b8563055166f9307761c367ab3e2ad7645b6dee0"
+ "digest": "c9d63118dcb445856917bb080460ab695cc78e715dcbba30ba18dffa9e906b27"
},
{
"name": "satellite_orbital",
"unicode": "1F6F0",
- "digest": "104b135e3736a4bcfd51a42dadb53bf3e00d7f85d77a94bcb86c6704fbfacd01"
+ "digest": "beb2f50e7f2b010e76bed9daa95d7329a93c783d3ebc4f0b797dd721c5e3d32d"
},
{
"name": "saxophone",
"unicode": "1F3B7",
- "digest": "1090da174ce8aa4f7d35025f65d5ac235e09310abde998d2a725ef3a989a2b75"
+ "digest": "dfd138634f6702a3b89b5a2a50016720eef3f800b0d1d8c9fe097808c9491e96"
},
{
"name": "scales",
"unicode": "2696",
- "digest": "b2984caa182b691a33650344708f47c61d6d319fd067760d7594c2ef60c1e27b"
+ "digest": "2280c026f16c6b92e0daa00bc14e718770f8d231c571ab439bde84d837cf31cc"
},
{
"name": "school",
"unicode": "1F3EB",
- "digest": "caf35260dc465a833521e4a0034201978fed41bbf72cd770756b3340c60e8a0c"
+ "digest": "af198b068a86ccad3daec4c6873e6b4735086c1ecbb3848182e70bae9aa3ee24"
},
{
"name": "school_satchel",
"unicode": "1F392",
- "digest": "a89a2cc46d24d57c2d6b95ed7a56ed829ae2f97b9e6201b2d5adc78c2b78518b"
+ "digest": "f670ae8aea67eb9d8aaa0bf2748c1cc3e503dcc1dbe999133afcdf21af046b24"
},
{
"name": "scissors",
"unicode": "2702",
- "digest": "a4e91127ac83acf5ebc64fbeca768cbbf24f2f0a484861c9c8104bee377b97ae"
+ "digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8"
},
{
"name": "scorpion",
"unicode": "1F982",
- "digest": "a090a96731bc1171b054b51abec4c9b36faa62708fd51ac48277ccf5e55d9d12"
+ "digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a"
},
{
"name": "scorpius",
"unicode": "264F",
- "digest": "1ad9bc1030a8f58f3f3223bac52c954cc7a0350805a9df7a42a26972c3b74728"
+ "digest": "a36404b408814c2ecb8fa8b61f5c5432dfcf54cae8c09cc67b8d0fadf7cbdc03"
},
{
"name": "scream",
"unicode": "1F631",
- "digest": "75d613786737ee9c0a74da7394b9ae190eacc7182164627ad8205ac64e4cc09a"
+ "digest": "916e4903a4b694da4b00f190f872a4e100e7736b7a2e6171fa1636f46bf646e6"
},
{
"name": "scream_cat",
"unicode": "1F640",
- "digest": "eee04ff27c2c6b57d698cb87b0af8064ba8313ffc13aa090e38cd5aa8c3d2f76"
+ "digest": "f1d3a6ff538064e7d5e0321bbc33aba44e8da703dc1894ef1403c0cd6d63d781"
},
{
"name": "scroll",
"unicode": "1F4DC",
- "digest": "b8205847649e3ce6b946f1d1da972ed015adde3841c62971b8169235f4b41c1f"
+ "digest": "9b2cb00860bcc2d20017cafb2ed9681b6232dc07273d489d75d53ce29e4ba3ab"
},
{
"name": "seat",
"unicode": "1F4BA",
- "digest": "054c4db0bc8939e9dd951a3f73e9ae4b3c31652784f4d304b509c2bd32f98e31"
+ "digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1"
},
{
"name": "secret",
"unicode": "3299",
- "digest": "77daef6e5c91d55228781ddec954a7089d1851297ec81daef6e813cd22915b5e"
+ "digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0"
},
{
"name": "see_no_evil",
"unicode": "1F648",
- "digest": "aa5883fe605aeaa172d16640b8347580f9cb7d85a596da1b13955f27b0b79297"
+ "digest": "3ff66d2e84b36d071d0a34f8e41cfd620a56b83131474ea50ed7803b635551ed"
},
{
"name": "seedling",
"unicode": "1F331",
- "digest": "a75ec929402de1e653fd6bc89e5be2f92fe5fe52f39e4b6c290eae3c59172b56"
+ "digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75"
},
{
"name": "seven",
"unicode": "0037-20E3",
- "digest": "c6a34020f6bb25871164fad44302a45c5bffced87f51dfbb816c2985ad7f6a1c"
+ "digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2"
},
{
"name": "shamrock",
"unicode": "2618",
- "digest": "530e6b987ecb9bcbf0d6e0e11bd075e7949873c784da4f9e1e1b47efd37e5058"
+ "digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488"
},
{
"name": "shaved_ice",
"unicode": "1F367",
- "digest": "fc22c3568f6be56771e83fd0e67b7eb3750041304d5d4979d3ec417f5201230e"
+ "digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74"
},
{
"name": "sheep",
"unicode": "1F411",
- "digest": "3e3656b82784164ca02c5d775db7245260f0119d2c1d35ba552a6dc75ef02544"
+ "digest": "c867c8e6e51768f1f51f4fe5abd3fbd5c1d69b01a3cb48b5fb94b6e2338a271c"
},
{
"name": "shell",
"unicode": "1F41A",
- "digest": "ff2f4f574b61bffd85c63bc2315c80d3cbcaba37a7c15a1f00783d312bd441d4"
+ "digest": "8983652d33ad6ab91195518cecb5a268a1c0ae603d271f0ddd756ff50058ddb3"
},
{
"name": "shield",
"unicode": "1F6E1",
- "digest": "062aec4a325da7b637c5710846c7e7319229be49b7e59f50428442a7ef725d60"
+ "digest": "763d0a56a62c51c730ccb0fbea38ab597cbf41a85ab968198e6ec35630d50aa5"
},
{
"name": "shinto_shrine",
"unicode": "26E9",
- "digest": "9768fe94142a7dc169703d3707b203f285a546455e29fe2bbf185d44f160d6d0"
+ "digest": "38a6d756c5aa9703510afa5076d75192f7814bbb6632394d4b8253d9ceda7f8c"
},
{
"name": "ship",
"unicode": "1F6A2",
- "digest": "f8d5b0c8ec66287b732d9171ac1913be02efb656de11501213a207d8a6c801e1"
+ "digest": "79c680845892a3e81ec6af2160ee07c29147155943e5daba6c76d04252014c20"
},
{
"name": "shirt",
"unicode": "1F455",
- "digest": "e2e72c323f3bfaea02e8cf52201aa144dc56ec0f25ec97d5f04ee6c2ee99104e"
+ "digest": "46c7253e15d7cac03699ddb1550fbb7565bbe487310f7e218c0583aa69f9d3c5"
},
{
"name": "shopping_bags",
"unicode": "1F6CD",
- "digest": "0194ba540c47e4fc6403be2df68f785d56810efc2dc011dfbf700f3778cb704a"
+ "digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b"
},
{
"name": "shower",
"unicode": "1F6BF",
- "digest": "c945120182392510348de9a957c2b77a4645d118691298a2ad660dafa62a859c"
+ "digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01"
},
{
"name": "signal_strength",
"unicode": "1F4F6",
- "digest": "7876ed9d602e1be746ca0629f072d85668d1f9715e9135745e803bdf89819a3c"
+ "digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447"
},
{
"name": "six",
"unicode": "0036-20E3",
- "digest": "b409f23b73e46393c7a814442816b5880c38ef12a7feb5505e71276c195e8ca9"
+ "digest": "cede9324261208d0fd5d00fcdfc0df0331944bd9cff4f40b30a582a641526c1c"
},
{
"name": "six_pointed_star",
"unicode": "1F52F",
- "digest": "4bc294dcbf4185250873b52b2fb5453fb7d80df912db929add6e4b7efc066363"
+ "digest": "9203e3b4f08af439ae0bfb6a7b29a02dceb027b6c2dc5463b524dfd314cbff4e"
},
{
"name": "ski",
"unicode": "1F3BF",
- "digest": "7ee81a2e2f7ff4e32dbf3d64b034e7542ec0c86d32e25eb125052e674943d75f"
+ "digest": "80f0ca8660ba373fef823af9e98e148c4ddb1e217eb6d0a0ea2bae2288b57570"
},
{
"name": "skier",
"unicode": "26F7",
- "digest": "49df9a4206ae0c7c2dbfc8a8b13fd3e14e6f7e750bd5a8581ab6a1626d4c165e"
+ "digest": "4fff0aa155367f551a59aed9657b8afa159173882b25db9cd8434293d1eed76d"
},
{
"name": "skull",
"unicode": "1F480",
- "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6"
+ "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
},
{
"name": "skeleton",
"unicode": "1F480",
- "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6"
+ "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
},
{
"name": "skull_crossbones",
"unicode": "2620",
- "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c"
+ "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
},
{
"name": "skull_and_crossbones",
"unicode": "2620",
- "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c"
+ "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
},
{
"name": "sleeping",
"unicode": "1F634",
- "digest": "4ead95079b1a542eedd0e5a0e93fddb318a002bdaffaa2fe5d8d7f20bf8143ed"
+ "digest": "1050a011509b56735c9f30a6fccc876256e2a4546dc6052e518151c8aca4b526"
},
{
"name": "sleeping_accommodation",
"unicode": "1F6CC",
- "digest": "10ee8cd925a75d7977b7cf004e08b5a8147b509ee4281e879a8b57c4a7c2cb04"
+ "digest": "2ce42c027d1d0947abc403c359fd668a7bc44f5ead2582e97f3db7dd4e22e5d5"
},
{
"name": "sleepy",
"unicode": "1F62A",
- "digest": "dea3b246bb8af1b28e200358e3d5d59c8bba1813f35a7f4a57ec568ef43591db"
+ "digest": "2ee9bb1f72ef99e0e33095ec2bbf7a58ffea0ff7d40b840f4cdba57be9de74b0"
},
{
"name": "slight_frown",
"unicode": "1F641",
- "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc"
+ "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
},
{
"name": "slightly_frowning_face",
"unicode": "1F641",
- "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc"
+ "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
},
{
"name": "slight_smile",
"unicode": "1F642",
- "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306"
+ "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
},
{
"name": "slightly_smiling_face",
"unicode": "1F642",
- "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306"
+ "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
},
{
"name": "slot_machine",
"unicode": "1F3B0",
- "digest": "9d516b389299431b608c89d3f02ac68d28cb8df2a780f2048923bbcfbb49f416"
+ "digest": "914184788f8cd865cd074dca25c22acee31f5498117bd9a6e78cae67e6601652"
},
{
"name": "small_blue_diamond",
"unicode": "1F539",
- "digest": "97389e82755dc43015089dee635072357ec347f0117b2d3e9b006c46514948ee"
+ "digest": "0b56d8e6b5ddf1f49fcc76e45e5fb2ee9f99ae6ffe682c26eaea4d9b7faac36c"
},
{
"name": "small_orange_diamond",
"unicode": "1F538",
- "digest": "67442d3b707501b7768f606115688373d13617ecf0b3b03ace0f1a6d38f66ddf"
+ "digest": "a2235830550e289c1608f2dcf5ede48f5c1a0eff45570699c39708c9677ab950"
},
{
"name": "small_red_triangle",
"unicode": "1F53A",
- "digest": "e0a556a3dd5bbf0290ed7c00eb6f6307dc2ea98d1fb3111fd85a7f46242a3638"
+ "digest": "8c2985c4e9ce42d2f3b35539b879bc36206c5ef749f39fbd1eac51bd2676e1e5"
},
{
"name": "small_red_triangle_down",
"unicode": "1F53B",
- "digest": "7a11dcb8a517df220493d471759e4f4bca0db3769e2d942bbf596a88a3e57f72"
+ "digest": "46bd328df2fbf5d0597596bbf00d2d5f6e0c65bcb8f3fb325df8ba0c25e445b5"
},
{
"name": "smile",
"unicode": "1F604",
- "digest": "46a7c3545b0038dfce6825d97544f6665f28512ad05c404d668e32ac599c7ecb"
+ "digest": "14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14"
},
{
"name": "smile_cat",
"unicode": "1F638",
- "digest": "c1db961f0fa261532b842816aca7ea7f6d8b461c7e930a1a1c91f96efd9db515"
+ "digest": "c35b76d6df100edb4022d762f47abfeb9f5e70886960c1d25908bd5d57ccb47e"
},
{
"name": "smiley",
"unicode": "1F603",
- "digest": "deeaaee64ebdd9fc0bcb719db75c3f7e0c33ddbcc97f6cd51f9f84377a4368ce"
+ "digest": "a89f31eb9d814636852517a7f4eadec59195e2ac2cc9f8d124f1a1cc0f775b4a"
},
{
"name": "smiley_cat",
"unicode": "1F63A",
- "digest": "85ad852cb3881c4b754af172fdfc6231af42578033ea9f2981ceae944c41e72f"
+ "digest": "3e66a113c5e3e73fb94be29084cb27986b6bdb0e78ab44785bf2a35a550e71bf"
},
{
"name": "smiling_imp",
"unicode": "1F608",
- "digest": "e777bdf186d89921df106d23bf002967b69afffd7e981b3cbb19f89630a06e87"
+ "digest": "3e02131d16525938f6facc7e097365dec7e13c8a0049a3be35fc29c80cc291b3"
},
{
"name": "smirk",
"unicode": "1F60F",
- "digest": "2e7fddd8bed33ef4b7d8c13320302b87a28203e576ef87bd43716952cf0b5ace"
+ "digest": "3c180d46f5574d6fca3bb68eb02517da60b7008843cb3e90f2f9620d0c8ee943"
},
{
"name": "smirk_cat",
"unicode": "1F63C",
- "digest": "9ca0721f4c18592b4b809ade8f716b95fa30cd31dd87d1e41db29a319becd705"
+ "digest": "0683c7f73e1f65984e91313607d7cca21d99acd4b2e9932f00e0fffd0ce90742"
},
{
"name": "smoking",
"unicode": "1F6AC",
- "digest": "3d14b3f0c57eb7a6a31ff371b0a454986533b79dbbeac78a76e4063478911b8d"
+ "digest": "baa9cb444bf0fe5c74358f981b19bc9e5c0415ced7f042baf93642282476ea61"
},
{
"name": "snail",
"unicode": "1F40C",
- "digest": "57d946c7ec84dfad71bc4f7a042927ec5712aef50c66d21af892b6c8a7faf5e1"
+ "digest": "5733bf3672ae4b2b3e090fa670aeac70dcbcc04ca5b13abc8c8e53b8b3d4ff33"
},
{
"name": "snake",
"unicode": "1F40D",
- "digest": "d084da540162288721364992f3b8059cbf2efd9f5b48f49a196ddbe23a073870"
+ "digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773"
},
{
"name": "snowboarder",
"unicode": "1F3C2",
- "digest": "de9e1767526de606f4908743af94cc17e89fdb0a2a44167d3d021ef09d033ab9"
+ "digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19"
},
{
"name": "snowflake",
"unicode": "2744",
- "digest": "e476863ccd7d7b549c6191fb25c121c6a467b4baef4683b7dc3e0a793c2e5d76"
+ "digest": "6556c918e181df01ba849e76c43972d5310439971e5d8fc2409d112c05bf0028"
},
{
"name": "snowman",
"unicode": "26C4",
- "digest": "792946b8446f2243d11b89d07c73a774be3abd36573f3918640b1ba8714270b5"
+ "digest": "6137456b2335e88e09c1859615eb22bb636355ef438f7a3949ad2f3d54478dd3"
},
{
"name": "snowman2",
"unicode": "2603",
- "digest": "571acabaa4d55782c4529b762423a7e34cb1fb6bb7852cbd013e2e846d8311d1"
+ "digest": "33ec75c22a13c81fa3c6b24a77ac1a08dc0dbe70b3716cf17b6702014d8a63fe"
},
{
"name": "sob",
"unicode": "1F62D",
- "digest": "562f02ab584bcbcf9ba73cf7fa7d7129965266abd28db2c73913b8c42f2f5aca"
+ "digest": "d1ed4b31861f9f9fd4e9c95a9c17530e2320a1b4cad6ececb1545ce25d65e4ce"
},
{
"name": "soccer",
"unicode": "26BD",
- "digest": "5fd0d534659b63dc862c65a80561b255bece0b76708fe8ecbae8e01b08d8cad0"
+ "digest": "6a3f2e6a9a0b64c3fbf8705995792091daf386a4112dba75507a1f556f662f84"
},
{
"name": "soon",
"unicode": "1F51C",
- "digest": "d2a1ab16a4056d80c827ea23f9332bb73235fc841b857cbf545062ff8aeed81d"
+ "digest": "a49d1bcfbac3e6ccc05b9a9863eff74b0eb8b4d4b22b8b0f7b2787fcba1c73cc"
},
{
"name": "sos",
"unicode": "1F198",
- "digest": "fadfe8337e133a6f05d205d0807f288e5c230db04cb09f3547ce0cb73cfcf48a"
+ "digest": "2fa7e0274383aeed6019eb9177e778d7aab8b88575b078b0ffeb77cd18df14b3"
},
{
"name": "sound",
"unicode": "1F509",
- "digest": "c0074b338fd461f1f9d1143b7f9b3781ddb3fd501ea79b2410630433a8e87b83"
+ "digest": "faaca7b315b2495cbc381468580d25f1d11362441c35bb43d8a914f2ec8202d2"
},
{
"name": "space_invader",
"unicode": "1F47E",
- "digest": "d264390004bd28d664dfda0069104be6db32ce477e23a95ac595bac2e29fd4e7"
+ "digest": "e75379cb5063f9a8861d762ad1886097c1697fbb61f2e4e8f531047955a4a2dd"
},
{
"name": "spades",
"unicode": "2660",
- "digest": "d1ad99a4fc20dfea881a9062a9f2109e483dbb5dea3b29e9653cb27ec57b4800"
+ "digest": "2c4d20f6a4893cfc62498d3f1f8f67577f39ed09f3e6682d8cb9cd8f365d30da"
},
{
"name": "spaghetti",
"unicode": "1F35D",
- "digest": "ac63f9ad143e236ce6068098e5330a333ade9cddfb3dd6b1457ea47ce9dcf7e9"
+ "digest": "6d3451dc0faa1913539edb99261448f51735f269b61193c53dfe63466c0191e8"
},
{
"name": "sparkle",
"unicode": "2747",
- "digest": "95b8f4f1bb6080cd1d7bd333c4724dbba43ed196dce72a2bbaab46c4a1bc0e48"
+ "digest": "7131163cd6c2f879110c86e9f068c33cf580f7c4b619449c41851fe6083402ee"
},
{
"name": "sparkler",
"unicode": "1F387",
- "digest": "3a296e4d0081ad1a566e111d218e352e1439bba9fd04e8a1eb9a8e36bd438cb7"
+ "digest": "88539ed8a13bd66e0c265c0913bd3ec2ddc4d95484323595713beb102221a1f6"
},
{
"name": "sparkles",
"unicode": "2728",
- "digest": "5ab280ea10c30e0e0b5a26ef52b8f47ad44a983330f7ef62ac0c0888752bbdb6"
+ "digest": "cf84d16b1c0a381d5a7ae79031872747c9a6887eab6e92cc4a10a4b8600ef506"
},
{
"name": "sparkling_heart",
"unicode": "1F496",
- "digest": "f145dab6b597c07e5a851176fabaf56dd857209645483d1acc1490d12c969113"
+ "digest": "b80b1ddef83b6528b309a194f6f2faf5acab603daeb9254523efc2b941bcb6d2"
},
{
"name": "speak_no_evil",
"unicode": "1F64A",
- "digest": "6eae2d066d39c4ba81e58a8327ed875c68bc9b1297c18dc0f5243e477a81040f"
+ "digest": "d2d7cfb4d471928a496bdc146890adc8422a68500b68115630b24c125d18e81f"
},
{
"name": "speaker",
"unicode": "1F508",
- "digest": "ea59c5a9d994808ff7937c300303e644b5f1ad41097e82f9e73ea6e1c718936c"
+ "digest": "dbca5f7181728d2ad67ff76fd566ffbdf53e333e7eeed341f54668bd47969413"
},
{
"name": "speaking_head",
"unicode": "1F5E3",
- "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b"
+ "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
},
{
"name": "speaking_head_in_silhouette",
"unicode": "1F5E3",
- "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b"
+ "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
},
{
"name": "speech_balloon",
"unicode": "1F4AC",
- "digest": "5dccfda46fc984583bc9eaece66e7e884f2a9eb12a69dbd3493035e3c862edd0"
+ "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
{
"name": "speech_left",
"unicode": "1F5E8",
- "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed"
+ "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
},
{
"name": "left_speech_bubble",
"unicode": "1F5E8",
- "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed"
+ "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
},
{
"name": "speech_right",
@@ -9562,122 +9562,122 @@
{
"name": "speedboat",
"unicode": "1F6A4",
- "digest": "553a288ab8eeb3dee7b9d1c92eba38016caef7658beaa828136ba1d6ba8ed08a"
+ "digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576"
},
{
"name": "spider",
"unicode": "1F577",
- "digest": "519f7243b5574102ce3f8953e5480812830a1feb32ae51e8573724c864338481"
+ "digest": "8411eac0c1b80926fd93cc1d6423e00b05d04c485b79ee232da8f1714e899a37"
},
{
"name": "spider_web",
"unicode": "1F578",
- "digest": "42959fae08a2162d6ee8c8706f823c5932f3801bc90da30d2ca9a48c3ff25572"
+ "digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23"
},
{
"name": "spy",
"unicode": "1F575",
- "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a"
+ "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
},
{
"name": "sleuth_or_spy",
"unicode": "1F575",
- "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a"
+ "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
},
{
"name": "spy_tone1",
"unicode": "1F575-1F3FB",
- "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94"
+ "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
},
{
"name": "sleuth_or_spy_tone1",
"unicode": "1F575-1F3FB",
- "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94"
+ "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
},
{
"name": "spy_tone2",
"unicode": "1F575-1F3FC",
- "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b"
+ "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
},
{
"name": "sleuth_or_spy_tone2",
"unicode": "1F575-1F3FC",
- "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b"
+ "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
},
{
"name": "spy_tone3",
"unicode": "1F575-1F3FD",
- "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7"
+ "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
},
{
"name": "sleuth_or_spy_tone3",
"unicode": "1F575-1F3FD",
- "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7"
+ "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
},
{
"name": "spy_tone4",
"unicode": "1F575-1F3FE",
- "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154"
+ "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
},
{
"name": "sleuth_or_spy_tone4",
"unicode": "1F575-1F3FE",
- "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154"
+ "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
},
{
"name": "spy_tone5",
"unicode": "1F575-1F3FF",
- "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d"
+ "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
},
{
"name": "sleuth_or_spy_tone5",
"unicode": "1F575-1F3FF",
- "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d"
+ "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
},
{
"name": "stadium",
"unicode": "1F3DF",
- "digest": "4356db5d2cdef8c40830638debaf1f50831130c12ae8d8dc3d9a6bd28fdaa1f7"
+ "digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f"
},
{
"name": "star",
"unicode": "2B50",
- "digest": "13240b8fada84e7555892996e9f9652503bf9b9a002056c2bae428d543abe2da"
+ "digest": "d78e5c1b78caed103e100150c10b08a9ca3ee30c243943d6fc3cc08f422122e9"
},
{
"name": "star2",
"unicode": "1F31F",
- "digest": "9b56c7548f6a222499d4e848576ea25eab837db72b207ebf8a62a451b35f758f"
+ "digest": "f91ac4afe3f5d4a52847ae8b4a9704b591e00399aebba553d150d7e34ee939fa"
},
{
"name": "star_and_crescent",
"unicode": "262A",
- "digest": "10b8a0771e415aa6610fa62185137aa1836c2bb3e82f1a3f601470e94f784923"
+ "digest": "1bf3d29e50034f5e7c0dccff0a3a533b74bfa9b489e357b2739a473311f1332a"
},
{
"name": "star_of_david",
"unicode": "2721",
- "digest": "5bc4d1038b8316281e01a9c575ded7ede0fc24c7593db5b5d36ca2e188aa5614"
+ "digest": "28a0bd0eeac9d0835ceb8425d72c2472464e863dd09b76a0ddc1c08cf1986402"
},
{
"name": "stars",
"unicode": "1F320",
- "digest": "23605eafc949feead3eca145a7ff5ee3b211a8bfd95621bd35dd05df532b97c6"
+ "digest": "837d9045316b8fb5e533457eac61241534f641eb78d8cb75f688f80fb8e8a7f0"
},
{
"name": "station",
"unicode": "1F689",
- "digest": "c346f12fff64161041af8492550c3541a6304e53f30288224ddd0c6fe08c4d6b"
+ "digest": "27a163ac0aea4ed247a121cae826eafc475977c68b0d888e9405bea14326ff56"
},
{
"name": "statue_of_liberty",
"unicode": "1F5FD",
- "digest": "56fa27ab059a9fd1f53aec47d9108277a3bf04a73186f36297cd1207c832ee31"
+ "digest": "f5a43599ab3f24ed3a78a745e06e2ac3e33107a292386ad81c67935ee5b22493"
},
{
"name": "steam_locomotive",
"unicode": "1F682",
- "digest": "d0ec2eb3d761ab6157e17eab1b8b4dec3a69f9becc4251592cbb67d71825e661"
+ "digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba"
},
{
"name": "stereo",
@@ -9692,7 +9692,7 @@
{
"name": "stew",
"unicode": "1F372",
- "digest": "12e6e4bf48a7296700e07a053d831dd67b70c308ca9522ca96e933a4d1ef6c5e"
+ "digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df"
},
{
"name": "stock_chart",
@@ -9702,212 +9702,212 @@
{
"name": "stop_button",
"unicode": "23F9",
- "digest": "57310962c7738a7da4f2a62cbd5e0b26d7aec357978267a0d8ca8e6cbd7ffb02"
+ "digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a"
},
{
"name": "stopwatch",
"unicode": "23F1",
- "digest": "c8e69c24f9da98dcb41c9c6355922d08a702f12a35667fbc5beb3f659430333d"
+ "digest": "9b6b9491a24d8ab4f896eb876da7973f028bd5e7c51a3767ba7e61bb6fbb2be0"
},
{
"name": "straight_ruler",
"unicode": "1F4CF",
- "digest": "55ff7182a3696461df52e3000708083f803bc8bf0f3c25dacb34175cc104b51d"
+ "digest": "cee31101767bd3f961363599924dc3790675d05a1285a8396428d2f91771c111"
},
{
"name": "strawberry",
"unicode": "1F353",
- "digest": "fd501e1fefb70242ac7c4dc30ad3d8c3ae200b263a832daedaa984906114afaf"
+ "digest": "5750a15e12f21259286ddbc3a8222a385b3b97a9f368897f42dd000060343174"
},
{
"name": "stuck_out_tongue",
"unicode": "1F61B",
- "digest": "1b49956cec511ee382177d95da77c8b6a9214a02c86bf7c6c6fd6cc9df3e9331"
+ "digest": "92dc42980a6dfdd7204fc874a762d6a0bbf0fdbfb5a7c0698fca04782e99fde6"
},
{
"name": "stuck_out_tongue_closed_eyes",
"unicode": "1F61D",
- "digest": "60a4d5d92550c6ad4db901d42c9f6434fe94fa3ddb353b6019a93d374d9485e9"
+ "digest": "434d25ac24cad7ba699eae876a25d9a99b584449cca50b124bf6aa7f20a83d51"
},
{
"name": "stuck_out_tongue_winking_eye",
"unicode": "1F61C",
- "digest": "d9c15ad1c4782a0391a79aeda2745127527385b0b5fc01c8d96c3f3b637a74ae"
+ "digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d"
},
{
"name": "sun_with_face",
"unicode": "1F31E",
- "digest": "56b14e92f68f8701fdc42763e1f4695ed352845f22bd5d412f827e5cf98dd83b"
+ "digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be"
},
{
"name": "sunflower",
"unicode": "1F33B",
- "digest": "817dea222a75bb6492c32b4b144d07f48295d7dd113e21760f90b18277612ebb"
+ "digest": "27d1161f50f932a6b26c404cf2e8f7083683ed0f2382d62b7472acccaa6eb695"
},
{
"name": "sunglasses",
"unicode": "1F60E",
- "digest": "16003cc5256397389889f52e0a5e14daea8d8c72f2ea660b8174529868cba9cd"
+ "digest": "966684382e5c59e98319e4c0ea7c304c61c2638ad5408faa49ce2c83c4416757"
},
{
"name": "sunny",
"unicode": "2600",
- "digest": "f68a774b7d574fc711111e17368b57c40d973d263c7e857544a09051d4592ab9"
+ "digest": "460fea4cbbdd1595450c1033a2ee5de7fea2e2f147822efa49f7e204812415aa"
},
{
"name": "sunrise",
"unicode": "1F305",
- "digest": "ce06a9321bc04605538a59f9fca8536d6209d7ded03120e5d2a0be955bb17ddf"
+ "digest": "7718a49636b0cdd1862ed67c7a9d6e72f471c2591ff0d912485b1be55d1ea115"
},
{
"name": "sunrise_over_mountains",
"unicode": "1F304",
- "digest": "286244ac2bec8c5c41cf8c7c439702fa525c57fab623f7f9bd7687db0adf75b2"
+ "digest": "743d0701cdbe2a814962363813c3153d3c5e62c3e410349f56d49dbb9581f356"
},
{
"name": "surfer",
"unicode": "1F3C4",
- "digest": "d17c7ea185ca5ef5a2950ef126ee14103bf7769acb419a20d08cc023f619e459"
+ "digest": "bb440775e9213430942015c37db8de58b5a561ee971b2a0f3993fc3f1d2554d4"
},
{
"name": "surfer_tone1",
"unicode": "1F3C4-1F3FB",
- "digest": "af66f2f26071b3ba8d7c795139055a58a857212f8cb1f51a507242ad7d2c49c7"
+ "digest": "a4937b030aca30b68bb644f37cf63c38aebce3c00b57d1c8a0ffe596b57d2f1e"
},
{
"name": "surfer_tone2",
"unicode": "1F3C4-1F3FC",
- "digest": "7a34e8b1fdad0a89bbb10333d241583ef018517fdd90f171ad7121de53776a3f"
+ "digest": "1c2a954a9c5284dedf0327d6f3c954c9fdd3953b848076d298874775ad8bf0a3"
},
{
"name": "surfer_tone3",
"unicode": "1F3C4-1F3FD",
- "digest": "b2f4cbd59a0aa93c7ee2bbb14ce55c8306dc25884377982a5f132ce6c074fa1d"
+ "digest": "418a3408b9ab026124f067c8597b500217e56bc28d9844a29eea5eee6f604ff8"
},
{
"name": "surfer_tone4",
"unicode": "1F3C4-1F3FE",
- "digest": "b16a02cfcc3606524cca9408e69c654fb83a162eaec8faae8dfd8ec67fe391c5"
+ "digest": "530870b9ac9f4d45ff750e264feb90b44fb93ca2852f323987b06f5f12fb5a4d"
},
{
"name": "surfer_tone5",
"unicode": "1F3C4-1F3FF",
- "digest": "b9a156e1aa57544b703db4e4a7773e244a3139e82c2c808c2e5a804fb524f512"
+ "digest": "40e11b1ae652cfd085d083377f1da24160065ed1b67403c6fa4655e6e44169ec"
},
{
"name": "sushi",
"unicode": "1F363",
- "digest": "d2709b51ee92997c7fafa1b1517259cb896819c8dc9ba98ae26e1d44ec810d4f"
+ "digest": "b924c621236ca3284b349b0509ae1043f2fc2c7f6d67615716f9717ada78c992"
},
{
"name": "suspension_railway",
"unicode": "1F69F",
- "digest": "48903e103ef00a068b0100b28319b1e41c6a4485cb564f0ca59422ec9d3b259c"
+ "digest": "cd3d21da79864f0c018b863e82fb0561fff3c5e3c065303cfcb89c3663d638ba"
},
{
"name": "sweat",
"unicode": "1F613",
- "digest": "8d684fa882bcbf07f4e91ea02a48cd61f22e7aa206162b8352c26fc19361ed4e"
+ "digest": "1aa771479aa1ac5eeea4bafbe93ebd85a0f692f6d869034f31e25b689c2e264d"
},
{
"name": "sweat_drops",
"unicode": "1F4A6",
- "digest": "fca48e255dff08dab97ef98b75c67f7504a13be8b90afac88b69a7b7e887e445"
+ "digest": "b575b85415bc9852cf6415d417ebf799167fde03c6819ebcaa24ae1b3dde8dab"
},
{
"name": "sweat_smile",
"unicode": "1F605",
- "digest": "0c8156554eec2396b5fee908da46484945db980d2ebc6dee57b4069a86826182"
+ "digest": "171b0d0845d46c33bedb6d3b39fb1ff366e22ba90685eedabebd91bb2b0680de"
},
{
"name": "sweet_potato",
"unicode": "1F360",
- "digest": "3ce74ea9bc14906a3d29a9592c0657aee8f7961d406992752f7580b16ca6bdd0"
+ "digest": "4b91920f0b87d42763313bc476f4c821a74e4c12dc1c92165a859dddeaaf8844"
},
{
"name": "swimmer",
"unicode": "1F3CA",
- "digest": "05f3aa8544e3b15837bb06ae47344633b3e60d64c572dc6638c4cee19d6e5506"
+ "digest": "2c4ed4a51aad99d9957ae11a219d5164db9748fc3a65002c6085a9f15adfa9e2"
},
{
"name": "swimmer_tone1",
"unicode": "1F3CA-1F3FB",
- "digest": "85a266a9131f6a1b37e758305ca43ffb46e3e07b0a465c5faefbdb5e5adeb7a4"
+ "digest": "48588f129ee4af52ca2e0f4594213391978601087cd607896b2f979ca077284b"
},
{
"name": "swimmer_tone2",
"unicode": "1F3CA-1F3FC",
- "digest": "f2afdc4d05a2694e663a420d5ad82bd48c92aedc4137d0fd3725bf08c41bd12a"
+ "digest": "fff209448524bd1ef4d6decabf6c1ead94c8d3d5b1bfb5e54f20cc8e139232fc"
},
{
"name": "swimmer_tone3",
"unicode": "1F3CA-1F3FD",
- "digest": "b87ecc38fb9e8eeeef8b120164d758d3f6a68a407053b03261354fd7f90f43b6"
+ "digest": "2003932cb2cf4ae9a10b23338bf375a9293fb18c0ecf91bdfae73be6eebb3800"
},
{
"name": "swimmer_tone4",
"unicode": "1F3CA-1F3FE",
- "digest": "a08629cf3484953b851b357c6a04891fb97ac15e70c376bbb82af47479835e1c"
+ "digest": "20b4bff9baa1c694ad98067dde834c56092f023b9664bec382c2e512232bd480"
},
{
"name": "swimmer_tone5",
"unicode": "1F3CA-1F3FF",
- "digest": "21d83f66b2ef3e348f9e14ec108b9a90262d9934039ebd573471d2bdcde68974"
+ "digest": "0ff8eb57c2be8e80a1bc6ba75b8d9ffb9bd8d3be636150c4c03399ec1886f218"
},
{
"name": "symbols",
"unicode": "1F523",
- "digest": "f33c3ce58374e23b8957c759016fdb5c56ef7fe812bd4e693ae8ff7574cf6bbf"
+ "digest": "2a2a79816c4d0751a0d73586eec5e63b410653d3c85cc968906bf1fc03d89b94"
},
{
"name": "synagogue",
"unicode": "1F54D",
- "digest": "b13402c3c5793ebf924335a87a9f69befb7a6c152fc2a288261b2c2d49842eb6"
+ "digest": "98569cdd7c61528963b67b7891dfa46025c5e810cbb22ee18ddb3bd85de2da69"
},
{
"name": "syringe",
"unicode": "1F489",
- "digest": "39e5e7530255ccf2ff35ec5c653568c8645a4711170c573117f796ea3438c44a"
+ "digest": "e1538e645ccc571227c994b71b3d1be2c4d072d8bd9c944a42ff4a11c91a34a6"
},
{
"name": "taco",
"unicode": "1F32E",
- "digest": "6b004ce7129e00abcc10278bba1b9c3d5ac71888b99bf353f9878d8e494e3e0d"
+ "digest": "e1e45aefdb7445faeae75c3831df6a3d6f2590fcdd48a20d847593c246df613b"
},
{
"name": "tada",
"unicode": "1F389",
- "digest": "956a180a1f18e3a1252761e5b3713324f63975ee1fe32168b59b60aa4dd8b72b"
+ "digest": "1d2e6cbb2a3244240bc70209715d2213d1efee2e370cccfbcc046c333ae2d650"
},
{
"name": "tanabata_tree",
"unicode": "1F38B",
- "digest": "d074457ba347687bfc8397ec62edee6325c411356216e7d43acd3f60628a0bb8"
+ "digest": "592f2907ffc1b914390e1a106c15120ff3607e99192158b94d237975647c5540"
},
{
"name": "tangerine",
"unicode": "1F34A",
- "digest": "1b46bb690458914220cba18c43d7ae0f6914adfee6dba7cf2bb58ed4e1854ad8"
+ "digest": "40c9ddcde1b0bcfaeb466629a87825eb8c2037835720cbee5e2fda04be3c8d0a"
},
{
"name": "taurus",
"unicode": "2649",
- "digest": "ea87fb3baa32605107d63b60847e4873ad9e21b7e7b652e3721cde777168670d"
+ "digest": "21cf24cb6410ab6596e2df8b3e242cc07f9dbb247eabc00c590fe184b373d068"
},
{
"name": "taxi",
"unicode": "1F695",
- "digest": "f44249c643a96d924e1eb35f67a133f3ca61128e610a880afaa09a73c7bcaf9d"
+ "digest": "c546cc743831cfbf0c15452767cf2a4faf3775066797e997ae7c1fcbe4eca479"
},
{
"name": "tea",
"unicode": "1F375",
- "digest": "56ab8c291de8320c5b339e1cfbe972696e4ea31c592cefa240eda9a3abdf4fa3"
+ "digest": "00e3f1e389fa58c4fcd8c53ebbf83d25872f4315845ab1984b35410ae65553d9"
},
{
"name": "telephone",
"unicode": "260E",
- "digest": "609104588e00039199a2fef3190ee6a7be5fca7cb09b36ffe5a7d800aac69d8d"
+ "digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7"
},
{
"name": "telephone_black",
@@ -9922,7 +9922,7 @@
{
"name": "telephone_receiver",
"unicode": "1F4DE",
- "digest": "e3bf6034de6cf2160893ba4990eba198185a6a3f9cd5767a63b048e41c297640"
+ "digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046"
},
{
"name": "telephone_white",
@@ -9937,52 +9937,52 @@
{
"name": "telescope",
"unicode": "1F52D",
- "digest": "abe0aca5f2c78105b0e9e4c8ee7a40adcd9bb013e7c49d568076459bade73556"
+ "digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495"
},
{
"name": "ten",
"unicode": "1F51F",
- "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40"
},
{
"name": "tennis",
"unicode": "1F3BE",
- "digest": "0a5fad3f7f35da0f37761e2279c148dbe154fa14c0e2a0749209b8b2b213a388"
+ "digest": "dc1600b4d8dce3d26259eb0d1c6ab042566565e3c1f2c96112210f1550a716fd"
},
{
"name": "tent",
"unicode": "26FA",
- "digest": "7ddf437d8d186e4e3c3e818d137518d590fa06098813c7fe20e1f2a9704feab2"
+ "digest": "30d9b17ac3219d4970ddf54d7c1a288b0ae50f7f3b82ed232c0b1b19ef585662"
},
{
"name": "thermometer",
"unicode": "1F321",
- "digest": "597d1714442698a22187fee4d57a2580322f7206c7d51e4519023824598ec08f"
+ "digest": "66616babbcaef256d7b652796c760e8e893cb950c073348a408fe70904f80f25"
},
{
"name": "thermometer_face",
"unicode": "1F912",
- "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687"
+ "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
},
{
"name": "face_with_thermometer",
"unicode": "1F912",
- "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687"
+ "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
},
{
"name": "thinking",
"unicode": "1F914",
- "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c"
+ "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
},
{
"name": "thinking_face",
"unicode": "1F914",
- "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c"
+ "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
},
{
"name": "thought_balloon",
"unicode": "1F4AD",
- "digest": "76c8513191641f0a79e878ccc0d83c4576984609810633f596db2f64cc684b7d"
+ "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e"
},
{
"name": "thought_left",
@@ -10007,7 +10007,7 @@
{
"name": "three",
"unicode": "0033-20E3",
- "digest": "ca0147a8f67cea3bc2516fa8deef4325188359559786c94ff0b27f90eef04b88"
+ "digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6"
},
{
"name": "thumbs_down_reverse",
@@ -10032,192 +10032,192 @@
{
"name": "thumbsdown",
"unicode": "1F44E",
- "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3"
+ "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
},
{
"name": "-1",
"unicode": "1F44E",
- "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3"
+ "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
},
{
"name": "thumbsdown_tone1",
"unicode": "1F44E-1F3FB",
- "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce"
+ "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
},
{
"name": "-1_tone1",
"unicode": "1F44E-1F3FB",
- "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce"
+ "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
},
{
"name": "thumbsdown_tone2",
"unicode": "1F44E-1F3FC",
- "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2"
+ "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
},
{
"name": "-1_tone2",
"unicode": "1F44E-1F3FC",
- "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2"
+ "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
},
{
"name": "thumbsdown_tone3",
"unicode": "1F44E-1F3FD",
- "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16"
+ "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
},
{
"name": "-1_tone3",
"unicode": "1F44E-1F3FD",
- "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16"
+ "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
},
{
"name": "thumbsdown_tone4",
"unicode": "1F44E-1F3FE",
- "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27"
+ "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
},
{
"name": "-1_tone4",
"unicode": "1F44E-1F3FE",
- "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27"
+ "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
},
{
"name": "thumbsdown_tone5",
"unicode": "1F44E-1F3FF",
- "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994"
+ "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
},
{
"name": "-1_tone5",
"unicode": "1F44E-1F3FF",
- "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994"
+ "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
},
{
"name": "thumbsup",
"unicode": "1F44D",
- "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee"
+ "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
},
{
"name": "+1",
"unicode": "1F44D",
- "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee"
+ "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
},
{
"name": "thumbsup_tone1",
"unicode": "1F44D-1F3FB",
- "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3"
+ "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
},
{
"name": "+1_tone1",
"unicode": "1F44D-1F3FB",
- "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3"
+ "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
},
{
"name": "thumbsup_tone2",
"unicode": "1F44D-1F3FC",
- "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356"
+ "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
},
{
"name": "+1_tone2",
"unicode": "1F44D-1F3FC",
- "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356"
+ "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
},
{
"name": "thumbsup_tone3",
"unicode": "1F44D-1F3FD",
- "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d"
+ "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
},
{
"name": "+1_tone3",
"unicode": "1F44D-1F3FD",
- "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d"
+ "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
},
{
"name": "thumbsup_tone4",
"unicode": "1F44D-1F3FE",
- "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa"
+ "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
},
{
"name": "+1_tone4",
"unicode": "1F44D-1F3FE",
- "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa"
+ "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
},
{
"name": "thumbsup_tone5",
"unicode": "1F44D-1F3FF",
- "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f"
+ "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
},
{
"name": "+1_tone5",
"unicode": "1F44D-1F3FF",
- "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f"
+ "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
},
{
"name": "thunder_cloud_rain",
"unicode": "26C8",
- "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c"
+ "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
},
{
"name": "thunder_cloud_and_rain",
"unicode": "26C8",
- "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c"
+ "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
},
{
"name": "ticket",
"unicode": "1F3AB",
- "digest": "a7654a5529535120da3c377e72cd1f7997bdc2dabf1d44b584f7df7852b158f9"
+ "digest": "b4326fe7761940216e6c76ee2928110a6b37bf913da9d694e96557e7c7c10420"
},
{
"name": "tickets",
"unicode": "1F39F",
- "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567"
+ "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
},
{
"name": "admission_tickets",
"unicode": "1F39F",
- "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567"
+ "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
},
{
"name": "tiger",
"unicode": "1F42F",
- "digest": "9ebe3117f5f1b589ff8164f8d87dcc275923e0db87121d2cee0fdb9b56dfc4ac"
+ "digest": "e139531e6c930bc46242dc0ed274661229de026b5419d8ea8f99fdb0f8a719ab"
},
{
"name": "tiger2",
"unicode": "1F405",
- "digest": "212c95dc60d52420a6320917fe3fdd0683b4edc1a2a2c4a1c60920d1f90f4bc3"
+ "digest": "f930cc8714198310d9b0edca6baff243ac5a3320f75fadb56fa5acc6fe34ff24"
},
{
"name": "timer",
"unicode": "23F2",
- "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff"
+ "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
},
{
"name": "timer_clock",
"unicode": "23F2",
- "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff"
+ "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
},
{
"name": "tired_face",
"unicode": "1F62B",
- "digest": "ad687a956388ec53ca1e301a0abe2f1e2cfb9f73cd543dd61a21c7335a42e332"
+ "digest": "775739bc9324517e614878ca0960d793df97775feeb62b14dbfb311a42a21802"
},
{
"name": "tm",
"unicode": "2122",
- "digest": "1156c8b0af40b336bbb6534b3302ac63eab009c4cd0476adcf1fc4669f04b647"
+ "digest": "7d9fafdb72d91860478fc185719f289f359eab2c368a132cb936a269e2ab6a24"
},
{
"name": "toilet",
"unicode": "1F6BD",
- "digest": "a4a24529c21e00e0861f4160c771f0e90aae8f6aee7550ad30d3dbb3fabbd4be"
+ "digest": "0d1b0dd0078f51104e8632a0726e1b3f075561a1ffa8a2546602de15798415d0"
},
{
"name": "tokyo_tower",
"unicode": "1F5FC",
- "digest": "6324f154f5f5c722044129e5bca03484aca1439911585e42c1c181ffa30b480c"
+ "digest": "73eaf6fd59d16396673afef620c6d928857d5cf616e95a40eaf2861686e0956a"
},
{
"name": "tomato",
"unicode": "1F345",
- "digest": "41bb6de095b27815eacb74a70aea8f7d4fe1ff947182b112001dd47ae7e45fbb"
+ "digest": "d092d8ad381d542e59b6a82b4f1ef0d10fc1ed48460952375c6c5c6258cea111"
},
{
"name": "tone1",
@@ -10247,72 +10247,72 @@
{
"name": "tongue",
"unicode": "1F445",
- "digest": "bf9dd7c65a8dc5d77eb013658a0a12a13f7b224a784e65e203d9584bb6b41427"
+ "digest": "286e9d2583c371431d6fc979dd4ab48981676da26baada51a846657a3654c19b"
},
{
"name": "tools",
"unicode": "1F6E0",
- "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9"
+ "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
},
{
"name": "hammer_and_wrench",
"unicode": "1F6E0",
- "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9"
+ "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
},
{
"name": "top",
"unicode": "1F51D",
- "digest": "d645030099aeb433307569e8e1c4342c1c411a8fefe50fdca7a3207a1a0db671"
+ "digest": "c9a9f25b17db014e76b6be54aa07ef89bb18f8adb41b3199d180a559ff1d9ea5"
},
{
"name": "tophat",
"unicode": "1F3A9",
- "digest": "1082fb2ee2e98fe65d21081b74ca59b07adef85043e2d36f25cac69db2d31fd3"
+ "digest": "43a45dfb5d6b57a63a0491f4e3ec780774c0301b53ed39a303a0bd803d16ed71"
},
{
"name": "track_next",
"unicode": "23ED",
- "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b"
+ "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
},
{
"name": "next_track",
"unicode": "23ED",
- "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b"
+ "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
},
{
"name": "track_previous",
"unicode": "23EE",
- "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1"
+ "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
},
{
"name": "previous_track",
"unicode": "23EE",
- "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1"
+ "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
},
{
"name": "trackball",
"unicode": "1F5B2",
- "digest": "8332503454ce42059d720c285fe2b15eb0562a0a4b234dccb0f3159bb30a91aa"
+ "digest": "32a819a3129429f797ad434d0c40e263dc236808e34878c599ed2304b43702f5"
},
{
"name": "tractor",
"unicode": "1F69C",
- "digest": "a41d304c41a85d966f6a7c301735fdbe2ae41f4471dd7dcd72023046ca2546d0"
+ "digest": "5e4686290f1a4c9953ae208340b7d276f25b3b2197a43e52469aeb6450e93997"
},
{
"name": "traffic_light",
"unicode": "1F6A5",
- "digest": "005f68d028fec8d9ae389cc2b23e1343a82c028eb32820d5e56f5c84eba315d1"
+ "digest": "d96aacade33d1ad3e0414f8a920513010f36eb7e5889774251c1d91148917ead"
},
{
"name": "train",
"unicode": "1F68B",
- "digest": "bf32893b7b9ecd248e8afe840624061746ac6ceb741e3e861ebfa46014f4bed4"
+ "digest": "7423d17e131df7aadaa350b5d39dcbce3b28de331ff8b6703a3b2d0093963f4b"
},
{
"name": "train2",
"unicode": "1F686",
- "digest": "08a9732453a0b4f68dd2d3d3879f04ee538f65897913b5a5157c0585132a374a"
+ "digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122"
},
{
"name": "train_diesel",
@@ -10327,7 +10327,7 @@
{
"name": "tram",
"unicode": "1F68A",
- "digest": "5a86d31f7ab677d967fecd75babc900b5169766d0228961912314c4c4d1d64ee"
+ "digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100"
},
{
"name": "triangle_round",
@@ -10342,62 +10342,62 @@
{
"name": "triangular_flag_on_post",
"unicode": "1F6A9",
- "digest": "d824c973d84cd62c845d64e546de87b094fda8f9972b6a33acd75e1a5ac19f75"
+ "digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da"
},
{
"name": "triangular_ruler",
"unicode": "1F4D0",
- "digest": "5576802d8bcb8836f473d9c7641ff666250c23c8476c676b253e577695025959"
+ "digest": "a0367dcf663ec934f1fc7c88bfaccc02b229a896f60930a66bb02241c933e501"
},
{
"name": "trident",
"unicode": "1F531",
- "digest": "70c1e8254da5b0e4552673b487503a20feeb249484d4596836b75de70220be82"
+ "digest": "ee45920845d3b35c2e45b934cf30ce97bfe2f24c5d72ef1ac6e0842e52b50fc1"
},
{
"name": "triumph",
"unicode": "1F624",
- "digest": "b09262121b0d3d9d017ded22d0fbb1acaa6ee8c9d38e9ac34292b390d97408fe"
+ "digest": "4aa44b8e1682c1269624a359f4b0bf613553683b883d947561ab169d7f85da0f"
},
{
"name": "trolleybus",
"unicode": "1F68E",
- "digest": "5af943836cc30c3b79160c70b6488c984fa63c104dce08c436597a93d30ff6f4"
+ "digest": "f610b4fd1123f06778a8e3bb8f738d5b0079aeb0b0926b6a63268c0dd0ee03ed"
},
{
"name": "trophy",
"unicode": "1F3C6",
- "digest": "c249938815042716db2b39cdece6715fabf9e56ed583270c451925e6c91f9191"
+ "digest": "50cfbedac18bf0fa5dec727643e15ec47f64068944b536e97518ee3be4f08006"
},
{
"name": "tropical_drink",
"unicode": "1F379",
- "digest": "352d903e813a27d2a74803322539b50a50aec0ca2ed7ab4a92ec480b1c226cb6"
+ "digest": "54144fce60d650f426b1edf09e47c70b2762222398c1fe40231881f074603a69"
},
{
"name": "tropical_fish",
"unicode": "1F420",
- "digest": "13a104ca9c326238ab8d85b60759629b4efaa836946fbe58d78d779443475f7b"
+ "digest": "fd92100aaa9328da35e6090388824921b9726b474d1432a926d2cf9c45ad6528"
},
{
"name": "truck",
"unicode": "1F69A",
- "digest": "13d381d6b43b42350a1e24c02296904b8fdc38c1bf0939fc7037850127e91f21"
+ "digest": "0d1571e58e900abc453df0ff683fe7acb5906ecbdd52ab35b7101074359faf18"
},
{
"name": "trumpet",
"unicode": "1F3BA",
- "digest": "df7fb48920ac0919ee2d7b30102016479f747a5d4dd25b3e18d9f17121d232d1"
+ "digest": "cea3614c309f5573f328f4603120dbe930016a35f0dfa400b0d968fe9fff2d55"
},
{
"name": "tulip",
"unicode": "1F337",
- "digest": "519a84336464b5dc8db57eecef3e5b8ed82ccfdaa0ed0fa9ef7bcf0e8acea1f8"
+ "digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086"
},
{
"name": "turkey",
"unicode": "1F983",
- "digest": "e87bff52ad3e301dc62f6832b8a6fcaf99db260a96263e4203a55ce3abda8cf8"
+ "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4"
},
{
"name": "turned_ok_hand",
@@ -10412,442 +10412,442 @@
{
"name": "turtle",
"unicode": "1F422",
- "digest": "388b3e75b931638a09f65b842d26e2cc87b200ba782dec871f84cddd71aaeaf3"
+ "digest": "588c35fb42c9502a908e9805517d4cc8c4ba4e74c9beed4035779fea1efe14f8"
},
{
"name": "tv",
"unicode": "1F4FA",
- "digest": "dba03be6482d6291599c7393b0f749c0de5c873d45c96a20ccc53b3e104a6a24"
+ "digest": "1279f3f3955a58dbbf74e248fc914b0bdba9c4c6b6a5176e9d12bf2750ecfeb4"
},
{
"name": "twisted_rightwards_arrows",
"unicode": "1F500",
- "digest": "5fcad0247576e10e683f353008749975e9371a4f66c0901a73c3a0c7803c63c7"
+ "digest": "fed07eebc2cf0d977ca0826bbd80defafbbcf118508444148f47b58949ebe27c"
},
{
"name": "two",
"unicode": "0032-20E3",
- "digest": "20ad722532a5073fff8aef0a5e890421da0ae97f0723a8a2cc503c13d24ba597"
+ "digest": "b346f51f6523b02ebcbd753256804e2f9cc1574c96aa634362bf9401dac2c661"
},
{
"name": "two_hearts",
"unicode": "1F495",
- "digest": "160cb11e3ed2ae1b20957d445c6c4b4bd604d067294818dfeeefba4562425eb9"
+ "digest": "6ded120a59aed790b441ec8fbbdea6f5cbfb4fa48e9e4b224cc29c9fde2d2e4c"
},
{
"name": "two_men_holding_hands",
"unicode": "1F46C",
- "digest": "923734704e544f7484fdb424bfe26f51ee07754db712cd151f8fbe955023a1ab"
+ "digest": "bfcf9e20a67d00262cdf6e85f1acd545dda91f2e370d68bfd41ce02f232a2987"
},
{
"name": "two_women_holding_hands",
"unicode": "1F46D",
- "digest": "58a40e7819cab3589ac81bb4fdc485b7196ee355544b54c6b00169028c260130"
+ "digest": "9d9d2b37a7f8e16fde1468dd8b5645003ea81ae4bf8bcf68471e2381845dd0dd"
},
{
"name": "u5272",
"unicode": "1F239",
- "digest": "b7e8ad52629a1f1fca77a5c9a51da87ce2b9a81f6af9bcbe9bec9552d398e9bf"
+ "digest": "01e6cb8f74ea3c19fdade59c2d13d158b90dc6b4b293421b2014b7478bf20870"
},
{
"name": "u5408",
"unicode": "1F234",
- "digest": "f359799d206cff6aae3af26eb8ad153abd38e817d4c70b2e5e5e8cf2f46e645e"
+ "digest": "084cdbd5436670ea4dc22010e269c1ab7b0432897b8675301e69120374bcdd14"
},
{
"name": "u55b6",
"unicode": "1F23A",
- "digest": "c40293bea0f148e76ca5152e830b1b474380fe259180fbf74fece1ccc9afd8a3"
+ "digest": "c1017023d20d4aae78d59342dd3bfc5282716ea0601d9a8c2476335cbf7a2e12"
},
{
"name": "u6307",
"unicode": "1F22F",
- "digest": "45449f7ae29da9e507c19d0f2b22f17f7cbd763f2ec87eb893be5bae49c7f78e"
+ "digest": "f459b092b974f459db1fb9cc13617a448b2e4f2b4dc46cc316d8c46af6e7d8bd"
},
{
"name": "u6708",
"unicode": "1F237",
- "digest": "b897ead8c952013975ce6f381cdb8c584ebe4015311ef87f2a332c8a9e155d75"
+ "digest": "928815abf5b30f92efe5168de0c7e6cf8c17899a03e358ab42f42667e0a4a04c"
},
{
"name": "u6709",
"unicode": "1F236",
- "digest": "8b2f792abc1313a1a58f2fb8b37ad68a964004c962535f7739131257b1331a05"
+ "digest": "f63a48ee06c892d24acec8b5634c021658d2ebde67a42d8faa86f27804a9f26d"
},
{
"name": "u6e80",
"unicode": "1F235",
- "digest": "fd982a56d4c492e63526b427bb948d7f155b0d5c414a68c7177698a71e72269b"
+ "digest": "489181d90a5e43068459530673a153e4af04fdad8514ec341ff7afbcfd366c3b"
},
{
"name": "u7121",
"unicode": "1F21A",
- "digest": "334f87a5254b58503d9f7a8ecc3d971a99839ec9c22c443469d72caca1750a48"
+ "digest": "9c50fd2ba14221affd2dcd3746322c2137dd75458493f4d385b544eb5bd8d6cd"
},
{
"name": "u7533",
"unicode": "1F238",
- "digest": "3c8e743ae9960e43b9fa0cc698018fcb2a52ae34d143f0561298191f9def019c"
+ "digest": "2b05819b380a2ea47cc5fde8fcce3d53922fd223d6f5bd83d696d44175b69f18"
},
{
"name": "u7981",
"unicode": "1F232",
- "digest": "a08bf39be3a54c076de79478c09b79c5c4d221853722870dd6e81abb78a4b64a"
+ "digest": "adbe12601b22972003ddebcb0bd1532b979aa9c78bfdc147511854b5014eabc0"
},
{
"name": "u7a7a",
"unicode": "1F233",
- "digest": "5dfb74a534a6490df989f84eac271c79d52f29313b6d43662dd0ff029794367c"
+ "digest": "b9ee0ec7bb0b86c3eb73d4dbbb91848c427bf356ae30a263b9b44bd9bd784482"
},
{
"name": "umbrella",
"unicode": "2614",
- "digest": "ff1191f6c11b82f5337f78aadb58af50c69abaf676a384b0473bf49004e4018f"
+ "digest": "0328a2f48b7df47905e2655460e524c0794ef12d3d7c32a049a10892d5662f77"
},
{
"name": "umbrella2",
"unicode": "2602",
- "digest": "aa7db9d6ed42dff847a8e5ee48a8eeff7a6e7f30de155a28951407f5aaa3dae2"
+ "digest": "2f6a58110dc590480a822a3ffa2b5bc86f295e0c994a4a632837d25d4cf9fc58"
},
{
"name": "unamused",
"unicode": "1F612",
- "digest": "efbbcaee6f3178afe509d74d13243ec6befe3112620a01e5079171eac4b32417"
+ "digest": "0d597088e3e7880918d0166e5c69243b18fe64afa31685c39bfdbc71494aa132"
},
{
"name": "underage",
"unicode": "1F51E",
- "digest": "ae9a300fa400a57b7216a0a040fb8a5f02236fbceeeceed58bfd953c87ad51fe"
+ "digest": "b6b194614ca714ac2b1c2c17b75fe5922c7fdadb3d1157ba89ab2a5d03494a67"
},
{
"name": "unicorn",
"unicode": "1F984",
- "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08"
+ "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
},
{
"name": "unicorn_face",
"unicode": "1F984",
- "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08"
+ "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
},
{
"name": "unlock",
"unicode": "1F513",
- "digest": "63dbef0855399254ae01cf4ef0676adebc1432ae1ee260b569c23ae8152deaf8"
+ "digest": "9554ef3a6a315938b873e77970d9b0212e61f13c6cc36e4f17f87acc930a9a53"
},
{
"name": "up",
"unicode": "1F199",
- "digest": "902a3ecbcd73099a28476b49bc9e7b06da6cc002ee584e0501e5b625fb515088"
+ "digest": "ff2554ccf08c7208b38794c5fa3d9a93a46ff191a49401195d8f740846121906"
},
{
"name": "upside_down",
"unicode": "1F643",
- "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53"
+ "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
},
{
"name": "upside_down_face",
"unicode": "1F643",
- "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53"
+ "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
},
{
"name": "urn",
"unicode": "26B1",
- "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e"
+ "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
},
{
"name": "funeral_urn",
"unicode": "26B1",
- "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e"
+ "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
},
{
"name": "v",
"unicode": "270C",
- "digest": "df85ad1a3ff365c3232a010701c9b25cd824d19fa2511422dee60ac231f457e3"
+ "digest": "9825bf440df289a8edf8ede494e8c778dc63c95f967f4d7bbea3245cf4f558ec"
},
{
"name": "v_tone1",
"unicode": "270C-1F3FB",
- "digest": "ce45db8de862b6f37d9208920d7c7c19335fac2cbff59b52be1ccbc01e3249da"
+ "digest": "76e358250d9ca519b60b8d7b6a32900700d784433dcc609e9442254a410f6e37"
},
{
"name": "v_tone2",
"unicode": "270C-1F3FC",
- "digest": "9036c8d793b02b4d2e6a4752b8ec319ec50efd6fcd6feef7b0671a63e5659acc"
+ "digest": "4081b674be8416136022523fa9f29ec70a0f7e3aa05ca13152606609f3fd003c"
},
{
"name": "v_tone3",
"unicode": "270C-1F3FD",
- "digest": "a94b95f7656d62b442c99f2643b96b0c6114683401a94cdda68405c37efecc4c"
+ "digest": "b6afb3a4c78384280610b953592d378241c75597a82aa6d16c86a993f8d8f3b0"
},
{
"name": "v_tone4",
"unicode": "270C-1F3FE",
- "digest": "5c75f74993856f2faeeaee68df7689056e60d30e8c573039db8303167f7d0a80"
+ "digest": "7ddc3cdd0138da2c8d7f6d8257ffdb8801496043e8a2395f93b0663447ac7fce"
},
{
"name": "v_tone5",
"unicode": "270C-1F3FF",
- "digest": "bb899672adb3c11f65983fbf9581de7f0a1bbac86fde146e799cea1126fe241e"
+ "digest": "a85dc5c589f0d1cf32f8bfa5c82e5c11c40b35439636914686a2f06f7359f539"
},
{
"name": "vertical_traffic_light",
"unicode": "1F6A6",
- "digest": "36296e03620f16d35e5cec195cd97f5b358dfdedcd43bc1b3f7988ff7e85ab47"
+ "digest": "8cfd49a8f96b15a8313ef855f2e234ea3fa58332e68896dea34760740de9f020"
},
{
"name": "vhs",
"unicode": "1F4FC",
- "digest": "f4be55f4c23a85e0caacbf569742c117c8fd52c189465a6560cbd2f8873ad74f"
+ "digest": "3fb1acaf25805cf86f8d40ee2c17cf25da587b7ca93b931167ab43fce041eee8"
},
{
"name": "vibration_mode",
"unicode": "1F4F3",
- "digest": "b9b8dfa3160c22f78b7d627cb52636d81ca6230a196cee5e94028e32e06b9a98"
+ "digest": "c9a8899222f46fe51dd8cee3e59f77c48268f0b7cfae2bcb34a791213acb1755"
},
{
"name": "video_camera",
"unicode": "1F4F9",
- "digest": "3bfaa24e5fb00145e3e4dd07ecf569dabbb3f211551e46085ef23cf23002cfc3"
+ "digest": "62e56f26c286a7964ef1021f0f23fcb4b38cdcfb5b5af569b472340c412c619a"
},
{
"name": "video_game",
"unicode": "1F3AE",
- "digest": "4dcbd76030e37d0f7429852991a5f3f126cbdedfc124ecad0ba29d227375f6e2"
+ "digest": "2787e302aa9e6fd7e9dc382c9bc7f5fbf244ef4940e08a4f9e80d33324f3032e"
},
{
"name": "violin",
"unicode": "1F3BB",
- "digest": "8ab7adc6e1e934f9e05009cd0a6d4da3136092c8f11c0606b91914be182206f5"
+ "digest": "1e69d531ce2b5d5bf1dd9470187dbbe76f479d14428834b6a9e2bf5296dc0ec9"
},
{
"name": "virgo",
"unicode": "264D",
- "digest": "aaa19752756d0cac949445de1d2b8bf1f75a071368ae0acf5002f4acdc34826f"
+ "digest": "0f75e9c228bc467fd0cec0f93f0e087c943bc5fb1d945fb0d4de53d07718388e"
},
{
"name": "volcano",
"unicode": "1F30B",
- "digest": "86c17d61d66bfa868c02f1d31daca22f077c096368ef53cd9bfb9914a2f0b273"
+ "digest": "41c92ef88ca533df342a0ebe59d2b676873bfa944c3988495b8a96060a9b8e16"
},
{
"name": "volleyball",
"unicode": "1F3D0",
- "digest": "b505684b13f814fbc08dc8ff652849328f46068276e0a24ae1961e2aff15868f"
+ "digest": "774a83357f7aee890b4d4383236f0a90946dbd7c86aaabadc5753dcc9b4c9d69"
},
{
"name": "vs",
"unicode": "1F19A",
- "digest": "e31bd8b48b88c21d717964d1360a7751684dd1e0b63fdd655f1a9ec10a952dfb"
+ "digest": "ac943e4c737459c2e1adbac8b71d3fdaebb704dbaf5713012e7a77beb09db1ef"
},
{
"name": "vulcan",
"unicode": "1F596",
- "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0"
+ "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers",
"unicode": "1F596",
- "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0"
+ "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
},
{
"name": "vulcan_tone1",
"unicode": "1F596-1F3FB",
- "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8"
+ "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1",
"unicode": "1F596-1F3FB",
- "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8"
+ "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
},
{
"name": "vulcan_tone2",
"unicode": "1F596-1F3FC",
- "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3"
+ "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2",
"unicode": "1F596-1F3FC",
- "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3"
+ "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
},
{
"name": "vulcan_tone3",
"unicode": "1F596-1F3FD",
- "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8"
+ "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3",
"unicode": "1F596-1F3FD",
- "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8"
+ "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
},
{
"name": "vulcan_tone4",
"unicode": "1F596-1F3FE",
- "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08"
+ "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4",
"unicode": "1F596-1F3FE",
- "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08"
+ "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
},
{
"name": "vulcan_tone5",
"unicode": "1F596-1F3FF",
- "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d"
+ "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
},
{
"name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5",
"unicode": "1F596-1F3FF",
- "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d"
+ "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
},
{
"name": "walking",
"unicode": "1F6B6",
- "digest": "8ec0b2207d4368422261bc58944c17dff2554b2356becfb18f21dd87425cd67b"
+ "digest": "ae77471fe1e8a734d11711cdb589f64347c35d6ee2fc10f6db16ac550c0557fa"
},
{
"name": "walking_tone1",
"unicode": "1F6B6-1F3FB",
- "digest": "9ee2224226326833fb0c9598c737fbd2f6bca1c81f082537e9f22ea1de4ff48e"
+ "digest": "3de871c234e1340ccf95338df7babd94d175cfcb17a57b5a74d950e0a31f03b1"
},
{
"name": "walking_tone2",
"unicode": "1F6B6-1F3FC",
- "digest": "4855d521e937d10d58eeb2bbada493699e31e1098128f81a9e3303bcf3edeb49"
+ "digest": "620eb7bfb753a331a5822b02bdaf08d8dde7b573efd210287a3d3dfdd84a40b9"
},
{
"name": "walking_tone3",
"unicode": "1F6B6-1F3FD",
- "digest": "82669cf7167054a3615add01059f87dbb809edac3889ee171d5994de90448000"
+ "digest": "ff39545acc2256006128f8c186433c28052b8c9aaec46fe06f25cff02c71f6b8"
},
{
"name": "walking_tone4",
"unicode": "1F6B6-1F3FE",
- "digest": "c11f03aa96248272f831f68b93c5b21b2ecbffeb1b4c1c13373bf539ee7db8f8"
+ "digest": "a9499d142392977a9b9e54fb957952359e9bdffce7ec2f1e8320523d185fb066"
},
{
"name": "walking_tone5",
"unicode": "1F6B6-1F3FF",
- "digest": "18238ee121a64211f6bcdbd475cee4ad6debe2bf421daba53d125aa005c26d10"
+ "digest": "b47a4c48ce40298f842f454fc1abccae70f69725d73ee2c80e4018f4c4065d7d"
},
{
"name": "waning_crescent_moon",
"unicode": "1F318",
- "digest": "96ef03ff85247877255a5ca3e8a8bb63f7d41f66531e8db61cbcd863e3ad7355"
+ "digest": "2ec7896eefcf821e0ea013556a17af59e997503662c07f080d0a84ab13ef4cf1"
},
{
"name": "waning_gibbous_moon",
"unicode": "1F316",
- "digest": "994223113ad151e6b42ee317a10dad18f86759a308e61ab88eeb10ab780aae67"
+ "digest": "ce2f5aca8fccdacaaf174d10da4e493e853e4608cc4d159aa3081d108a8b58d5"
},
{
"name": "warning",
"unicode": "26A0",
- "digest": "a702e51efd1a3ab425eada008ccf694f38a71db14bb710edacc2e206d61f5ca3"
+ "digest": "745f1d203958f42bf37ecb5909cd0819934e300308ba0ff20964c8c203092f90"
},
{
"name": "wastebasket",
"unicode": "1F5D1",
- "digest": "afecb31aaf5078298ab9f7c5da29a49ce0cdefe477ee50889be9c0e43ccf1799"
+ "digest": "221a1b6d9975051038d9d97e18a16556cdf4254a6bca4c29bf1c51f306c79f2a"
},
{
"name": "watch",
"unicode": "231A",
- "digest": "410334c87b8552f601f4ea1b7e36582a8b22f11b804d5ab1008d4af2b5a0cbe6"
+ "digest": "acc0c96751404a789b3085f10425cf34f942185215df459515d2439cde3efc6b"
},
{
"name": "water_buffalo",
"unicode": "1F403",
- "digest": "d1becfaea464372c46e5442c6030ea355806ce5864c2435c123a9bb3a2c3c5eb"
+ "digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1"
},
{
"name": "watermelon",
"unicode": "1F349",
- "digest": "88dd78812520c44080c79fe8cb1825bc713e5155da2ce8c73286333749e7035e"
+ "digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a"
},
{
"name": "wave",
"unicode": "1F44B",
- "digest": "5103c49914ff1a2d76a1ab6db2530ddd9f48b98b708ab15292ceadf28873c939"
+ "digest": "cddbd764d471604446cbaca91f77f6c4119d1cfc2c856732ca0eaac4593cb736"
},
{
"name": "wave_tone1",
"unicode": "1F44B-1F3FB",
- "digest": "ef2d79f377d09dedd1e900b2f4e4a2412bf562cd88484f71c52d465053f8aae9"
+ "digest": "cf40797437ddf68ec0275f337e6aac4bed81e28da7636d56c9f817ddf8e2b30a"
},
{
"name": "wave_tone2",
"unicode": "1F44B-1F3FC",
- "digest": "d323e6e2e9ce035bc11b98226d46ab393dfdf3909d99e7a828b51950e6574656"
+ "digest": "12c8a3e82c03ee35a734c642be482ba2d9d5948dacf91ec1fda243316dd4a0d0"
},
{
"name": "wave_tone3",
"unicode": "1F44B-1F3FD",
- "digest": "8a8a386d53252455c20d6b235c462fd9cb3b20c9c19c67e67b3dece4621b5cf6"
+ "digest": "ebcaef43e21b475f76de811d4f4d1a67d9393973b57b03876e02164345a2ba4a"
},
{
"name": "wave_tone4",
"unicode": "1F44B-1F3FE",
- "digest": "a8281c2ab9cf6e2b3d3cad24707fe412ec2398195530b716a2617477416c0432"
+ "digest": "7df7b70cf76766836ba146c3d91b6104930c384450cf2688426e60c1c06a1fc8"
},
{
"name": "wave_tone5",
"unicode": "1F44B-1F3FF",
- "digest": "5ccbee95bfc180580c8a02b88146110c4d132b8ea618dd6a58f03c1db921d58d"
+ "digest": "8dfdba6aeff5d7dfd807467d431a137547726b34d021f1a5a0b74e155d270ea7"
},
{
"name": "wavy_dash",
"unicode": "3030",
- "digest": "b5b67fc12938801a98ff22b6f7b566c603f58c183737fa740a500724879f0e99"
+ "digest": "7b1968474f01d12fd09a1f2572282927138d9e9d6a3642de4bf68af80a8c3738"
},
{
"name": "waxing_crescent_moon",
"unicode": "1F312",
- "digest": "20446122d170b18f88ea71524f6747d42b97f9d765c52e676e5163fee58ec379"
+ "digest": "852d7e55a19074d061fa3aa80d6b1e7e87a9280bdf44d94bbdbbe6d59178b1be"
},
{
"name": "waxing_gibbous_moon",
"unicode": "1F314",
- "digest": "4324e43d4d45e6333f7379c9feb8efd3093d76f3920d7dc5ad3c615e76104998"
+ "digest": "a3a1c7cc72521a3f74929789a90e1c35d81ac86e21225c9f844d718d8940e3b3"
},
{
"name": "wc",
"unicode": "1F6BE",
- "digest": "cb7c5d35bf11149d12cda2c0897cb6038e043127055bbe2e8e33c9b422d6d8fc"
+ "digest": "4b95d54e0b53e4b705277917653503b32d6a143c2eaf6c547bc8e01c2dc23659"
},
{
"name": "weary",
"unicode": "1F629",
- "digest": "29a291033a1b67eda3710dffae42d63fcfa663e37dab728c236172f3e877fe8f"
+ "digest": "3528f85540996cd5b562efe5421c495fc1bb414dc797bc20062783ae1b730847"
},
{
"name": "wedding",
"unicode": "1F492",
- "digest": "6c7d874f464c9c76b0d767135aa40ced94089b5f71d373098b47488d7f3ef7c4"
+ "digest": "980f3522cc4c19c3096e668032ea2cd19e7900cdc4b73bbb1c9b4c4d28dc78af"
},
{
"name": "whale",
"unicode": "1F433",
- "digest": "94168acda6ba502b64ea50ff4aaafb7e6258d7c6806e91f090c8a3c46edc5b6d"
+ "digest": "6368fe4bc4a7f68aa2bd5386686a5f1b159feacbec16d59515f2b6e5d01adfbd"
},
{
"name": "whale2",
"unicode": "1F40B",
- "digest": "e1cde2308bd510b2449c96e88ffec796856f98b19ceedc1cd7e9ea009dae1417"
+ "digest": "ccd3edf88167965f2abc18631ffb80e2532f728da35bc0c11144376685da18e8"
},
{
"name": "wheel_of_dharma",
"unicode": "2638",
- "digest": "bbd6927697c22a1c3e56fd0c9933d9e00dbf120505fe48d02cb486bcd67a8b2c"
+ "digest": "4a0a13fcd507b9621686c8090bf340aa8770c064e0e3eb576fbae1229000d6da"
},
{
"name": "wheelchair",
"unicode": "267F",
- "digest": "513f759acf528f6a7e39d9de1d171c3faebe645c9cf3bd86b185123016beef95"
+ "digest": "f5250f2b4b5b4ffe6a6f77d30865c3f5d7173fc91aee547869589b2a96da91c8"
},
{
"name": "white_check_mark",
"unicode": "2705",
- "digest": "a0b3bf7c4fb131e7a9fab5169ea4094e2665e02cedaa091f0d6e78609b2f17ed"
+ "digest": "45eb17bde6e503f22c8579d6e4d507ad6557a15f9eaad14aa716ec9ba1540876"
},
{
"name": "white_circle",
@@ -10857,142 +10857,142 @@
{
"name": "white_flower",
"unicode": "1F4AE",
- "digest": "a3efea4950e09994f5e9d3d16f0728969238302304a6cce90b293c56e9a3e20c"
+ "digest": "ace093b310eeefdecf4a4bdaf4fbcbb568457b0191ac80778a466ac5f3f4025a"
},
{
"name": "white_large_square",
"unicode": "2B1C",
- "digest": "99c4442a65f2e3c568f45aed9e74590206c517a716557f4d741d967c9f42ed40"
+ "digest": "0db6957ee9ff7325b534b730fc05345a63d4ed9060f0f816807d0dcf004baa3e"
},
{
"name": "white_medium_small_square",
"unicode": "25FD",
- "digest": "a1edfeb4e540dcc020ba5dde19f7a18d90966788baa5382a22a0f9038d593f01"
+ "digest": "d79689981a7b38211c60a025a81e44fd39ac6ea4062e227cae3aab8f51572cd4"
},
{
"name": "white_medium_square",
"unicode": "25FB",
- "digest": "794c2339ca71bb6d65ac488fb7b5dc4f0a2412f30890d2c4ece53cdbf52ba78b"
+ "digest": "6c4ce26d3f69667219f29ea18b04f3e79373024426275f25936e09a683e9a4fc"
},
{
"name": "white_small_square",
"unicode": "25AB",
- "digest": "9c4c308070a0c4524993cc36feaa778aad8f0df9f209b82d28b1f3811c441bc4"
+ "digest": "ae0d35a6bbba4592b89b2f0f1f2d183efb2f93cf2a2136c0c195aab72f0bb1c8"
},
{
"name": "white_square_button",
"unicode": "1F533",
- "digest": "f46e18c7250c874d1b4d6117eda741d86a081352e76f3d019dd64af2669fa4bb"
+ "digest": "797f3d9e44e88e940ffc118e52d0f709eec2ef14b13bdf873ad4b0c96cc0b042"
},
{
"name": "white_sun_cloud",
"unicode": "1F325",
- "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070"
+ "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
},
{
"name": "white_sun_behind_cloud",
"unicode": "1F325",
- "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070"
+ "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
},
{
"name": "white_sun_rain_cloud",
"unicode": "1F326",
- "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1"
+ "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
},
{
"name": "white_sun_behind_cloud_with_rain",
"unicode": "1F326",
- "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1"
+ "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
},
{
"name": "white_sun_small_cloud",
"unicode": "1F324",
- "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185"
+ "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
},
{
"name": "white_sun_with_small_cloud",
"unicode": "1F324",
- "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185"
+ "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
},
{
"name": "wind_blowing_face",
"unicode": "1F32C",
- "digest": "20bdeb8e39dc637792ac9fbee031c5791889f3126e83556ba51f98809c19763c"
+ "digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f"
},
{
"name": "wind_chime",
"unicode": "1F390",
- "digest": "1fc26f33ce13b6a969bb76e914de054ec5d1c7c4cd1dc5ee8fea5f3149f794d8"
+ "digest": "1b1b212fbd74a9edc62aee7ffab9bcf91d3a9f69bffb2be4b7fd527914c14ced"
},
{
"name": "wine_glass",
"unicode": "1F377",
- "digest": "7dfcf9c5195a20fd2745b19e102910392b0fc8f1650b98ab81957807841935e0"
+ "digest": "d99107d6809386bc5e219aa58ee4930d27b7c3a6d2b10deb9f523df369f766d1"
},
{
"name": "wink",
"unicode": "1F609",
- "digest": "404ac6c920414ca35894da1d97b3b2fabe92bd09569274eb5798fbb297129036"
+ "digest": "56e29994a47335a901d0c98fa141d26faae8f647a860517bd3615fa980921885"
},
{
"name": "wolf",
"unicode": "1F43A",
- "digest": "ebadd7766c4a314b4027c32435a2f5727a6283123dfb8834e10251cbfc07ca2f"
+ "digest": "4a983f5ec8ec0872fcde7890e17605b1229064e5e194b6fca1c4259068d1caed"
},
{
"name": "woman",
"unicode": "1F469",
- "digest": "9f0dbb5d1e0db4f008141582dcb6413f5aebaa13e191349c976a435b2bee0956"
+ "digest": "a06a22a48eeb3aeb885321358fe234e97797ed33be17f52d232ce2830cfbcd97"
},
{
"name": "woman_tone1",
"unicode": "1F469-1F3FB",
- "digest": "c1f2a503481fdd96cfbfa7d556500f8e0da0cea1c72ed1078ecbb6962221c22a"
+ "digest": "c2e4b135c1dac6a0b002569a6ccd9d098f6cb18481c68b5d9115e11241a0978d"
},
{
"name": "woman_tone2",
"unicode": "1F469-1F3FC",
- "digest": "bf78b3a8f7424037069f8ac337e154ef185f55026c71a6cf6dbe15eb42ef9813"
+ "digest": "4848e650051214a53c4cd9f6d3d94158f77f65ecb34f891789de34ee0a713006"
},
{
"name": "woman_tone3",
"unicode": "1F469-1F3FD",
- "digest": "4ccd70a2052b932b3395ac0a957c05815327dc8082fd461abcd797411db8ce05"
+ "digest": "b6f751ad47da019cdfb9d6d78f9610adb92120abf204c30df79a9150b57dbdee"
},
{
"name": "woman_tone4",
"unicode": "1F469-1F3FE",
- "digest": "71b5efc4a410102e60048ca05f87587384a6db309f3be94109a4f92ea97072dc"
+ "digest": "fd27d3a669dc34313fbfe518df7dc2ded3ade5dde695f8d773afe87bf8a8b0d4"
},
{
"name": "woman_tone5",
"unicode": "1F469-1F3FF",
- "digest": "91a1cd015731f4db501c276a8236eb0665e4dc7aa1891e2a67b8d3e543fbea9c"
+ "digest": "9ae9b14dfff40fa60a565d89479727feeba4fd6ffea9acb353a81b14aba751d4"
},
{
"name": "womans_clothes",
"unicode": "1F45A",
- "digest": "599332c0b863a40fd0c319e4e0f52ae847326a96d180c288e0466b3ac308a27e"
+ "digest": "d12a27810780fe5cd8118ed4587e0c4e70dbe9bcd014c6866fe6a8c9c7c55698"
},
{
"name": "womans_hat",
"unicode": "1F452",
- "digest": "231ff55c3fa56d8fb5731fe41f547e67ffacfdde82286f45d4ca65a2d2821239"
+ "digest": "52a0255b3483085bd125d39b74516ab6a81003964f44995c2fac821e7ff93086"
},
{
"name": "womens",
"unicode": "1F6BA",
- "digest": "f971429456b543804412490af2e27e0b14d0d536a156db898bce67b136e1b563"
+ "digest": "7e38964006f8b28dfa2b3e9b2b16553bb50c18a63455f556b0bff35ee172137e"
},
{
"name": "worried",
"unicode": "1F61F",
- "digest": "e017f636e79b9301f3a06471a5f3513ba7dbb9b97938de1140c1df4c32fd8844"
+ "digest": "5a073985e1344bc34201ef94a491f7f2b946f5828c9fdbc57eeb2dcd87ac3a6b"
},
{
"name": "wrench",
"unicode": "1F527",
- "digest": "c9ded4f7f496bad8691677226310bbd31bb485722ea479bc7a68a2b4ef9d55d9"
+ "digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4"
},
{
"name": "writing_hand",
@@ -11007,76 +11007,76 @@
{
"name": "writing_hand_tone1",
"unicode": "270D-1F3FB",
- "digest": "38e64e6dca4847a12aef8a117c113b2025d841501c4bc8188c57d0c8a4f1e34d"
+ "digest": "2c7e2108e1990490b681343c1b01b4183d4f18fbdef792f113b2f87595e0dad0"
},
{
"name": "writing_hand_tone2",
"unicode": "270D-1F3FC",
- "digest": "2b2d0ac2701ae707c31d9c85feb2e3700e11398701e2b0519338897817d53baf"
+ "digest": "87ec8d44f472d301adbcbd50d8c852b609e46584057f59cc1527401db363c1bf"
},
{
"name": "writing_hand_tone3",
"unicode": "270D-1F3FD",
- "digest": "85d67f90ff8bd2e7157f28fd857e6730b660a7eb82eb5350f57671f728ce725b"
+ "digest": "4a48ddef91f7264e8fa9cca223554db22b3a2e3153e94b88d146644ea6dd661e"
},
{
"name": "writing_hand_tone4",
"unicode": "270D-1F3FE",
- "digest": "056c05c201b3d0972433f00910967ad7334e37726e2956fee053ec2e1a9153c7"
+ "digest": "e5254564a1f91e42ee59f359d8cd26f52abdc04dca8f3b37cb2f140cb7f71390"
},
{
"name": "writing_hand_tone5",
"unicode": "270D-1F3FF",
- "digest": "95c59157d301ee08990e4302fd9bdd7953e1d1abed09636d0837d84e44f53ba6"
+ "digest": "61299bf86d83d323ca3e6052c535ae66c6f7b3d9866a37db0464223b8bc28523"
},
{
"name": "x",
"unicode": "274C",
- "digest": "1d256b0015b9cbdeaa4558f9241782c89d86c79a42e507621f7949c56a90b6c0"
+ "digest": "3e5a7918e31ddefdf1ce73972365e2f0bfd2917d6a450c1a278c108349c9425d"
},
{
"name": "yellow_heart",
"unicode": "1F49B",
- "digest": "e869a80266b4379a8d82988fef25e187632bfb076ae619f576e416906cd688a7"
+ "digest": "a1098f2f04c29754cc9974324508386787d4d803b57cf691d42de414cb2679d6"
},
{
"name": "yen",
"unicode": "1F4B4",
- "digest": "8f3d801c687e585e4497123c5c91a8b0c558578deec6a8c1591b25e64a3a8992"
+ "digest": "944daaeb3f6369c807c0e63b106cee1360040f7800a70c0d942a992f25a55da7"
},
{
"name": "yin_yang",
"unicode": "262F",
- "digest": "e8ea4c686518ad6165e15ed67b529f2f1e20d648aa2ecb7e9bff5a6067dd3fea"
+ "digest": "5ee8d13dacf41306a09237bfcff6abeef110331b40eb7d6e80600628c1327545"
},
{
"name": "yum",
"unicode": "1F60B",
- "digest": "d9c97bbf6bdb6e39977437680f0b37c9335306c51e01114056ae1d4c9c85b0e0"
+ "digest": "31a89088c21bd7a74a3a26d731a907d1bc49436300a9f9c55248703cf7ef44c7"
},
{
"name": "zap",
"unicode": "26A1",
- "digest": "37588734c7fe330ae35e6ee99e7cf4183e8fe1bc01f6bbbc6293b21076a338cb"
+ "digest": "9f8144ae6f866129aea41bbf694b0c858ef9352a139969e57cd8db73385f52c3"
},
{
"name": "zero",
"unicode": "0030-20E3",
- "digest": "519c927db8264d5379ab2c6a18656ea6dd1ceb2afc92eb48563bf86af4697571"
+ "digest": "1b27b5c904defadbdd28ace67a6be5c277ff043297db7cd9f672bbf84e37fa1a"
},
{
"name": "zipper_mouth",
"unicode": "1F910",
- "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9"
+ "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
},
{
"name": "zipper_mouth_face",
"unicode": "1F910",
- "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9"
+ "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
},
{
"name": "zzz",
"unicode": "1F4A4",
- "digest": "f07c56d2d55c0a886c26a8e3d49a9adeab54cc1a0c0354ea8d3bf23aaed3176d"
+ "digest": "b3313d0c44a59fa9d4ce9f7eb4d07ff71dfc8bb01798154250f27cdcf3c693b5"
}
] \ No newline at end of file
diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb
new file mode 100644
index 00000000000..27acc75dcc4
--- /dev/null
+++ b/generator_templates/active_record/migration/create_table_migration.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class <%= migration_class_name %> < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :<%= table_name %> do |t|
+<% attributes.each do |attribute| -%>
+<% if attribute.password_digest? -%>
+ t.string :password_digest<%= attribute.inject_options %>
+<% else -%>
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
+<% end -%>
+<% end -%>
+<% if options[:timestamps] %>
+ t.timestamps null: false
+<% end -%>
+ end
+<% attributes_with_index.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+<% end -%>
+ end
+end
diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb
new file mode 100644
index 00000000000..06bdea11367
--- /dev/null
+++ b/generator_templates/active_record/migration/migration.rb
@@ -0,0 +1,55 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class <%= migration_class_name %> < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+<%- if migration_action == 'add' -%>
+ def change
+<% attributes.each do |attribute| -%>
+ <%- if attribute.reference? -%>
+ add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- else -%>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- if attribute.has_index? -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ <%- end -%>
+<%- end -%>
+ end
+<%- elsif migration_action == 'join' -%>
+ def change
+ create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t|
+ <%- attributes.each do |attribute| -%>
+ <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ end
+ end
+<%- else -%>
+ def change
+<% attributes.each do |attribute| -%>
+<%- if migration_action -%>
+ <%- if attribute.reference? -%>
+ remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- else -%>
+ <%- if attribute.has_index? -%>
+ remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ <%- end -%>
+ remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- end -%>
+<%- end -%>
+<%- end -%>
+ end
+<%- end -%>
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 5fd9c30cb42..3d7d67510a8 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -26,36 +26,41 @@ module API
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
helpers ::API::Helpers
- mount ::API::Groups
+ mount ::API::AwardEmoji
+ mount ::API::Branches
+ mount ::API::Builds
+ mount ::API::CommitStatuses
+ mount ::API::Commits
+ mount ::API::DeployKeys
+ mount ::API::Files
mount ::API::GroupMembers
- mount ::API::Users
- mount ::API::Projects
- mount ::API::Repositories
+ mount ::API::Groups
+ mount ::API::Internal
mount ::API::Issues
- mount ::API::Milestones
- mount ::API::Session
+ mount ::API::Keys
+ mount ::API::Labels
+ mount ::API::LicenseTemplates
mount ::API::MergeRequests
+ mount ::API::Milestones
+ mount ::API::Namespaces
mount ::API::Notes
- mount ::API::Internal
- mount ::API::SystemHooks
- mount ::API::ProjectSnippets
- mount ::API::ProjectMembers
- mount ::API::DeployKeys
mount ::API::ProjectHooks
+ mount ::API::ProjectMembers
+ mount ::API::ProjectSnippets
+ mount ::API::Projects
+ mount ::API::Repositories
+ mount ::API::Runners
mount ::API::Services
- mount ::API::Files
- mount ::API::Commits
- mount ::API::CommitStatuses
- mount ::API::Namespaces
- mount ::API::Branches
- mount ::API::Labels
+ mount ::API::Session
mount ::API::Settings
- mount ::API::Keys
+ mount ::API::SidekiqMetrics
+ mount ::API::Subscriptions
+ mount ::API::SystemHooks
mount ::API::Tags
+ mount ::API::Templates
+ mount ::API::Todos
mount ::API::Triggers
- mount ::API::Builds
+ mount ::API::Users
mount ::API::Variables
- mount ::API::Runners
- mount ::API::Licenses
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
new file mode 100644
index 00000000000..c4fa1838b5a
--- /dev/null
+++ b/lib/api/award_emoji.rb
@@ -0,0 +1,115 @@
+module API
+ class AwardEmoji < Grape::API
+ before { authenticate! }
+ AWARDABLES = [Issue, MergeRequest]
+
+ resource :projects do
+ AWARDABLES.each do |awardable_type|
+ awardable_string = awardable_type.to_s.underscore.pluralize
+ awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+
+ [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
+ ].each do |endpoint|
+ # Get a list of project +awardable+ award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # Example Request:
+ # GET /projects/:id/issues/:awardable_id/award_emoji
+ get endpoint do
+ if can_read_awardable?
+ awards = paginate(awardable.award_emoji)
+ present awards, with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ # Get a specific award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # award_id (required) - The ID of the award
+ # Example Request:
+ # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
+ get "#{endpoint}/:award_id" do
+ if can_read_awardable?
+ present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ # Award a new Emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or mr
+ # name (required) - The name of a award_emoji (without colons)
+ # Example Request:
+ # POST /projects/:id/issues/:awardable_id/award_emoji
+ post endpoint do
+ required_attributes! [:name]
+
+ not_found!('Award Emoji') unless can_read_awardable?
+
+ award = awardable.award_emoji.new(name: params[:name], user: current_user)
+
+ if award.save
+ present award, with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji #{award.errors.messages}")
+ end
+ end
+
+ # Delete a +awardables+ award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # award_emoji_id (required) - The ID of an award emoji
+ # Example Request:
+ # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+ delete "#{endpoint}/:award_id" do
+ award = awardable.award_emoji.find(params[:award_id])
+
+ unauthorized! unless award.user == current_user || current_user.admin?
+
+ award.destroy
+ present award, with: Entities::AwardEmoji
+ end
+ end
+ end
+ end
+
+ helpers do
+ def can_read_awardable?
+ ability = "read_#{awardable.class.to_s.underscore}".to_sym
+
+ can?(current_user, ability, awardable)
+ end
+
+ def awardable
+ @awardable ||=
+ begin
+ if params.include?(:note_id)
+ noteable.notes.find(params[:note_id])
+ else
+ noteable
+ end
+ end
+ end
+
+ def noteable
+ if params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ else
+ user_project.merge_requests.find(params[:merge_request_id])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 231840148d9..d467eb9d474 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -25,7 +25,7 @@ module API
# branch (required) - The name of the branch
# Example Request:
# GET /projects/:id/repository/branches/:branch
- get ':id/repository/branches/:branch', requirements: { branch: /.*/ } do
+ get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
@branch = user_project.repository.branches.find { |item| item.name == params[:branch] }
not_found!("Branch") unless @branch
present @branch, with: Entities::RepoObject, project: user_project
@@ -39,8 +39,7 @@ module API
# Example Request:
# PUT /projects/:id/repository/branches/:branch/protect
put ':id/repository/branches/:branch/protect',
- requirements: { branch: /.*/ } do
-
+ requirements: { branch: /.+/ } do
authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch])
@@ -59,8 +58,7 @@ module API
# Example Request:
# PUT /projects/:id/repository/branches/:branch/unprotect
put ':id/repository/branches/:branch/unprotect',
- requirements: { branch: /.*/ } do
-
+ requirements: { branch: /.+/ } do
authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch])
@@ -101,7 +99,7 @@ module API
# Example Request:
# DELETE /projects/:id/repository/branches/:branch
delete ":id/repository/branches/:branch",
- requirements: { branch: /.*/ } do
+ requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 2b104f90aa7..d36047acd1f 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -13,7 +13,6 @@ module API
# Example Request:
# GET /projects/:id/builds
get ':id/builds' do
-
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
@@ -33,10 +32,10 @@ module API
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
+ return not_found! unless user_project.commit(params[:sha])
- builds = commit.builds.order('id DESC')
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
builds = filter_builds(builds, params[:scope])
present paginate(builds), with: Entities::Build,
@@ -142,7 +141,7 @@ module API
return not_found!(build) unless build
return forbidden!('Build is not retryable') unless build.retryable?
- build = Ci::Build.retry(build)
+ build = Ci::Build.retry(build, current_user)
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
@@ -166,6 +165,26 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
+
+ # Keep the artifacts to prevent them from being deleted
+ #
+ # Parameters:
+ # id (required) - the id of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # POST /projects/:id/builds/:build_id/artifacts/keep
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build && build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
end
helpers do
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 9bcd33ff19e..323a7086890 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -22,8 +22,8 @@ module API
not_found!('Commit') unless user_project.commit(params[:sha])
- ci_commits = user_project.ci_commits.where(sha: params[:sha])
- statuses = ::CommitStatus.where(commit: ci_commits)
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ statuses = ::CommitStatus.where(pipeline: pipelines)
statuses = statuses.latest unless parse_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
statuses = statuses.where(stage: params[:stage]) if params[:stage].present?
@@ -50,7 +50,7 @@ module API
commit = @project.commit(params[:sha])
not_found! 'Commit' unless commit
- # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline)
+ # Since the CommitStatus is attached to Ci::Pipeline (in the future Pipeline)
# We need to always have the pipeline object
# To have a valid pipeline object that can be attached to specific MR
# Other CI service needs to send `ref`
@@ -64,11 +64,11 @@ module API
ref = branches.first
end
- ci_commit = @project.ensure_ci_commit(commit.sha, ref)
+ pipeline = @project.ensure_pipeline(commit.sha, ref)
name = params[:name] || params[:context]
- status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref])
- status ||= GenericCommitStatus.new(project: @project, commit: ci_commit, user: current_user)
+ status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
+ status ||= GenericCommitStatus.new(project: @project, pipeline: pipeline, user: current_user)
status.update(attrs)
case params[:state].to_s
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 93a3a5ce089..4a11c8e3620 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -107,6 +107,8 @@ module API
break if opts[:line_code]
end
+
+ opts[:type] = LegacyDiffNote.name if opts[:line_code]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2870a6a40ef..9076a0c3831 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -30,7 +30,7 @@ module API
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 :two_factor_enabled?, as: :two_factor_enabled
expose :external
end
@@ -58,6 +58,14 @@ module API
expose :path, :path_with_namespace
end
+ class SharedGroup < Grape::Entity
+ expose :group_id
+ expose :group_name do |group_link, options|
+ group_link.group.name
+ end
+ expose :group_access, as: :group_access_level
+ end
+
class Project < Grape::Entity
expose :id, :description, :default_branch, :tag_list
expose :public?, as: :public
@@ -66,7 +74,8 @@ module API
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
- expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at
+ expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled
+ expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :creator_id
expose :namespace
@@ -76,6 +85,9 @@ module API
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
+ expose :shared_with_groups do |project, options|
+ SharedGroup.represent(project.project_group_links.all, options)
+ end
end
class ProjectMember < UserBasic
@@ -87,14 +99,12 @@ module API
class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility_level
expose :avatar_url
-
- expose :web_url do |group, options|
- Gitlab::Routing.url_helpers.group_url(group)
- end
+ expose :web_url
end
class GroupDetail < Group
expose :projects, using: Entities::Project
+ expose :shared_projects, using: Entities::Project
end
class GroupMember < UserBasic
@@ -170,15 +180,22 @@ module API
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
+
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user])
end
expose :user_notes_count
+ expose :upvotes, :downvotes
+ end
+
+ class ExternalIssue < Grape::Entity
+ expose :title
+ expose :id
end
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
- expose :upvotes, :downvotes
+ expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
@@ -216,8 +233,16 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
# upvote? and downvote? are deprecated, always return false
- expose :upvote?, as: :upvote
- expose :downvote?, as: :downvote
+ expose(:upvote?) { |note| false }
+ expose(:downvote?) { |note| false }
+ end
+
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
end
class MRNote < Grape::Entity
@@ -227,9 +252,9 @@ module API
class CommitNote < Grape::Entity
expose :note
- expose(:path) { |note| note.diff_file_name }
- expose(:line) { |note| note.diff_new_line }
- expose(:line_type) { |note| note.diff_line_type }
+ expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? }
+ expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? }
+ expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? }
expose :author, using: Entities::UserBasic
expose :created_at
end
@@ -259,6 +284,31 @@ module API
expose :id, :project_id, :group_id, :group_access
end
+ class Todo < Grape::Entity
+ expose :id
+ expose :project, using: Entities::BasicProjectDetails
+ expose :author, using: Entities::UserBasic
+ expose :action_name
+ expose :target_type
+
+ expose :target do |todo, options|
+ Entities.const_get(todo.target_type).represent(todo.target, options)
+ end
+
+ expose :target_url do |todo, options|
+ target_type = todo.target_type.underscore
+ target_url = "namespace_project_#{target_type}_url"
+ target_anchor = "note_#{todo.note_id}" if todo.note_id?
+
+ Gitlab::Application.routes.url_helpers.public_send(target_url,
+ todo.project.namespace, todo.project, todo.target, anchor: target_anchor)
+ end
+
+ expose :body
+ expose :state
+ expose :created_at
+ end
+
class Namespace < Grape::Entity
expose :id, :path, :kind
end
@@ -307,6 +357,10 @@ module API
class Label < Grape::Entity
expose :name, :color, :description
expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
+
+ expose :subscribed do |label, options|
+ label.subscribed?(options[:current_user])
+ end
end
class Compare < Grape::Entity
@@ -344,6 +398,7 @@ module API
expose :signin_enabled
expose :gravatar_enabled
expose :sign_in_text
+ expose :after_sign_up_text
expose :created_at
expose :updated_at
expose :home_page_url
@@ -357,6 +412,8 @@ module API
expose :restricted_signup_domains
expose :user_oauth_applications
expose :after_sign_out_path
+ expose :container_registry_token_expire_delay
+ expose :repository_storage
end
class Release < Grape::Entity
@@ -403,6 +460,8 @@ module API
class RunnerDetails < Runner
expose :tag_list
+ expose :run_untagged
+ expose :locked
expose :version, :revision, :platform, :architecture
expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
@@ -424,11 +483,7 @@ module API
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 :commit, with: RepoCommit
expose :runner, with: Runner
end
@@ -451,5 +506,13 @@ module API
expose(:limitations) { |license| license.meta['limitations'] }
expose :content
end
+
+ class TemplatesList < Grape::Entity
+ expose :name
+ end
+
+ class Template < Grape::Entity
+ expose :name, :content
+ end
end
end
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
index ab9b7c602b5..dbe5bb08d3f 100644
--- a/lib/api/group_members.rb
+++ b/lib/api/group_members.rb
@@ -77,7 +77,7 @@ module API
member = group.group_members.find_by(user_id: params[:user_id])
if member.nil?
- render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404)
+ render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404)
else
member.destroy
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 91e420832f3..9d8b8d737a9 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -95,8 +95,7 @@ module API
# GET /groups/:id/projects
get ":id/projects" do
group = find_group(params[:id])
- projects = group.projects
- projects = filter_projects(projects)
+ projects = GroupProjectsFinder.new(group).execute(current_user)
projects = paginate projects
present projects, with: Entities::Project
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 40c967453fb..77e407b54c5 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -2,16 +2,20 @@ module API
module Helpers
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
- SUDO_HEADER ="HTTP_SUDO"
+ SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
def parse_boolean(value)
[ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
end
+ def find_user_by_private_token
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+ User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ end
+
def current_user
- private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
+ @current_user ||= (find_user_by_private_token || doorkeeper_guard)
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
return nil
@@ -29,11 +33,11 @@ module API
@current_user
end
- def sudo_identifier()
+ def sudo_identifier
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
# Regex for integers
- if !!(identifier =~ /^[0-9]+$/)
+ if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
@@ -95,6 +99,17 @@ module API
end
end
+ def find_project_label(id)
+ label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
+ label || not_found!('Label')
+ end
+
+ def find_project_issue(id)
+ issue = user_project.issues.find(id)
+ not_found! unless can?(current_user, :read_issue, issue)
+ issue
+ end
+
def paginate(relation)
relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
add_pagination_headers(data)
@@ -397,5 +412,23 @@ module API
error!(errors[:access_level], 422) if errors[:access_level].any?
not_found!(errors)
end
+
+ def send_git_blob(repository, blob)
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ header(*Gitlab::Workhorse.send_git_blob(repository, blob))
+ end
+
+ def send_git_archive(repository, ref:, format:)
+ header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
+ end
+
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ Entities::ExternalIssue
+ else
+ Entities::Issue
+ end
+ end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 3ac7b50c4ce..d5dfba5e0cc 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -13,6 +13,7 @@ module API
# action - git action (git-upload-pack or git-receive-pack)
# ref - branch name
# forced_push - forced_push
+ # protocol - Git access protocol being used, e.g. HTTP or SSH
#
helpers do
@@ -20,11 +21,23 @@ module API
@wiki ||= params[:project].end_with?('.wiki') &&
!Project.find_with_namespace(params[:project])
end
+
+ def project
+ @project ||= begin
+ project_path = params[:project]
+
+ # Check for *.wiki repositories.
+ # Strip out the .wiki from the pathname before finding the
+ # project. This applies the correct project permissions to
+ # the wiki repository as well.
+ project_path.chomp!('.wiki') if wiki?
+
+ Project.find_with_namespace(project_path)
+ end
+ end
end
post "/allowed" do
- Gitlab::Metrics.action = 'Grape#/internal/allowed'
-
status 200
actor =
@@ -34,24 +47,26 @@ module API
User.find_by(id: params[:user_id])
end
- project_path = params[:project]
-
- # Check for *.wiki repositories.
- # Strip out the .wiki from the pathname before finding the
- # project. This applies the correct project permissions to
- # the wiki repository as well.
- project_path.chomp!('.wiki') if wiki?
-
- project = Project.find_with_namespace(project_path)
+ protocol = params[:protocol]
access =
if wiki?
- Gitlab::GitAccessWiki.new(actor, project)
+ Gitlab::GitAccessWiki.new(actor, project, protocol)
else
- Gitlab::GitAccess.new(actor, project)
+ Gitlab::GitAccess.new(actor, project, protocol)
end
- access.check(params[:action], params[:changes])
+ access_status = access.check(params[:action], params[:changes])
+
+ response = { status: access_status.status, message: access_status.message }
+
+ if access_status.status
+ # Return the repository full path so that gitlab-shell has it when
+ # handling ssh commands
+ response[:repository_path] = project.repository.path_to_repo
+ end
+
+ response
end
#
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 40928749481..8a03a41e9c5 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -51,7 +51,7 @@ module API
# GET /issues?labels=foo,bar
# GET /issues?labels=foo,bar&state=opened
get do
- issues = current_user.issues
+ issues = current_user.issues.inc_notes_with_associations
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues.reorder(issuable_order_by => issuable_sort)
@@ -59,6 +59,41 @@ module API
end
end
+ resource :groups do
+ # Get a list of group issues
+ #
+ # Parameters:
+ # id (required) - The ID of a group
+ # state (optional) - Return "opened" or "closed" issues
+ # 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`
+ #
+ # Example Requests:
+ # GET /groups/:id/issues
+ # GET /groups/:id/issues?state=opened
+ # GET /groups/:id/issues?state=closed
+ # GET /groups/:id/issues?labels=foo
+ # GET /groups/:id/issues?labels=foo,bar
+ # GET /groups/:id/issues?labels=foo,bar&state=opened
+ # GET /groups/:id/issues?milestone=1.0.0
+ # GET /groups/:id/issues?milestone=1.0.0&state=closed
+ get ":id/issues" do
+ group = find_group(params[:id])
+
+ params[:state] ||= 'opened'
+ params[:group_id] = group.id
+ params[:milestone_title] = params.delete(:milestone)
+ params[:label_name] = params.delete(:labels)
+ params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort]
+
+ issues = IssuesFinder.new(current_user, params).execute
+
+ present paginate(issues), with: Entities::Issue, current_user: current_user
+ end
+ end
+
resource :projects do
# Get a list of project issues
#
@@ -82,7 +117,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.visible_to_user(current_user)
+ issues = user_project.issues.inc_notes_with_associations.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?
@@ -103,8 +138,7 @@ module API
# Example Request:
# 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)
+ @issue = find_project_issue(params[:issue_id])
present @issue, with: Entities::Issue, current_user: current_user
end
@@ -234,42 +268,6 @@ module API
authorize!(:destroy_issue, issue)
issue.destroy
end
-
- # Subscribes to a project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # POST /projects/:id/issues/:issue_id/subscription
- post ':id/issues/:issue_id/subscription' do
- issue = user_project.issues.find(params[:issue_id])
-
- if issue.subscribed?(current_user)
- not_modified!
- else
- issue.toggle_subscription(current_user)
- present issue, with: Entities::Issue, current_user: current_user
- end
- end
-
- # Unsubscribes from a project issue
- #
- # Parameters:
- # id (required) - The ID of a project
- # issue_id (required) - The ID of a project issue
- # Example Request:
- # DELETE /projects/:id/issues/:issue_id/subscription
- delete ':id/issues/:issue_id/subscription' do
- issue = user_project.issues.find(params[:issue_id])
-
- if issue.subscribed?(current_user)
- issue.unsubscribe(current_user)
- present issue, with: Entities::Issue, current_user: current_user
- else
- not_modified!
- end
- end
end
end
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 4af6bef0fa7..c806829d69e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -11,7 +11,7 @@ module API
# Example Request:
# GET /projects/:id/labels
get ':id/labels' do
- present user_project.labels, with: Entities::Label
+ present user_project.labels, with: Entities::Label, current_user: current_user
end
# Creates a new label
@@ -36,7 +36,7 @@ module API
label = user_project.labels.create(attrs)
if label.valid?
- present label, with: Entities::Label
+ present label, with: Entities::Label, current_user: current_user
else
render_validation_error!(label)
end
@@ -90,7 +90,7 @@ module API
attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name)
if label.update(attrs)
- present label, with: Entities::Label
+ present label, with: Entities::Label, current_user: current_user
else
render_validation_error!(label)
end
diff --git a/lib/api/licenses.rb b/lib/api/license_templates.rb
index 187d2c04703..d0552299ed0 100644
--- a/lib/api/licenses.rb
+++ b/lib/api/license_templates.rb
@@ -1,16 +1,16 @@
module API
- # Licenses API
- class Licenses < Grape::API
+ # License Templates API
+ class LicenseTemplates < Grape::API
PROJECT_TEMPLATE_REGEX =
- /[\<\{\[]
- (project|description|
- one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]/xi.freeze
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
- /[\<\{\[]
- (fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]/xi.freeze
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
# Get the list of the available license templates
#
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 7e78609ecb9..4fcdf8968c9 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -41,7 +41,7 @@ module API
#
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
- merge_requests = user_project.merge_requests
+ merge_requests = user_project.merge_requests.inc_notes_with_associations
unless params[:iid].nil?
merge_requests = filter_by_iid(merge_requests, params[:iid])
@@ -218,6 +218,7 @@ module API
# 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
+ # sha (optional) - When present, must have the HEAD SHA of the source branch
# Example:
# PUT /projects/:id/merge_requests/:merge_request_id/merge
#
@@ -227,18 +228,21 @@ module API
# 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
+ not_allowed! unless merge_request.mergeable_state?
- render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
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?
+ if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active?
::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
execute(merge_request)
else
@@ -325,43 +329,7 @@ module API
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, current_user: current_user
- end
-
- # Subscribes to a merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of a merge request
- # Example Request:
- # POST /projects/:id/issues/:merge_request_id/subscription
- post "#{path}/subscription" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
- if merge_request.subscribed?(current_user)
- not_modified!
- else
- merge_request.toggle_subscription(current_user)
- present merge_request, with: Entities::MergeRequest, current_user: current_user
- end
- end
-
- # Unsubscribes from a merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of a merge request
- # Example Request:
- # DELETE /projects/:id/merge_requests/:merge_request_id/subscription
- delete "#{path}/subscription" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
- if merge_request.subscribed?(current_user)
- merge_request.unsubscribe(current_user)
- present merge_request, with: Entities::MergeRequest, current_user: current_user
- else
- not_modified!
- end
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
end
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 132043cf3f7..7a0cb7c99f3 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -115,7 +115,6 @@ module API
issues = IssuesFinder.new(current_user, finder_params).execute
present paginate(issues), with: Entities::Issue, current_user: current_user
end
-
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 71a53e6f0d6..8bfa998dc53 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -19,20 +19,24 @@ module API
# GET /projects/:id/issues/:noteable_id/notes
# 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}"])
-
- # 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
+ @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
+
+ if can?(current_user, noteable_read_ability_name(@noteable), @noteable)
+ # 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
+ else
+ not_found!("Notes")
+ end
end
# Get a single +noteable+ note
@@ -45,13 +49,14 @@ module API
# GET /projects/:id/issues/:noteable_id/notes/:note_id
# GET /projects/:id/snippets/:noteable_id/notes/:note_id
get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
+ @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
@note = @noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user)
- if @note.cross_reference_not_visible_for?(current_user)
- not_found!("Note")
- else
+ if can_read_note
present @note, with: Entities::Note
+ else
+ not_found!("Note")
end
end
@@ -136,5 +141,11 @@ module API
end
end
end
+
+ helpers do
+ def noteable_read_ability_name(noteable)
+ "read_#{noteable.class.to_s.underscore}".to_sym
+ end
+ end
end
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index ccca65cbe1c..6bb70bc8bc3 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -28,7 +28,6 @@ module API
present @hook, with: Entities::ProjectHook
end
-
# Add hook to project
#
# Parameters:
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
index 4aefdf319c6..6a0b3e7d134 100644
--- a/lib/api/project_members.rb
+++ b/lib/api/project_members.rb
@@ -4,7 +4,6 @@ module API
before { authenticate! }
resource :projects do
-
# Get a project team members
#
# Parameters:
@@ -46,7 +45,7 @@ module API
required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one
- project_member = user_project.project_member_by_id(params[:user_id])
+ project_member = user_project.project_member(params[:user_id])
if project_member.nil?
project_member = user_project.project_members.new(
user_id: params[:user_id],
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index cc2c7a0c503..0cc1edd65c8 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -44,7 +44,7 @@ module API
# Example Request:
# GET /projects/starred
get '/starred' do
- @projects = current_user.starred_projects
+ @projects = current_user.viewable_starred_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
present @projects, with: Entities::Project
@@ -94,6 +94,7 @@ module API
# builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # container_registry_enabled (optional)
# shared_runners_enabled (optional)
# namespace_id (optional) - defaults to user namespace
# public (optional) - if true same as setting visibility_level = 20
@@ -112,6 +113,7 @@ module API
:builds_enabled,
:wiki_enabled,
:snippets_enabled,
+ :container_registry_enabled,
:shared_runners_enabled,
:namespace_id,
:public,
@@ -143,6 +145,7 @@ module API
# builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # container_registry_enabled (optional)
# shared_runners_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional)
@@ -206,6 +209,7 @@ module API
# builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # container_registry_enabled (optional)
# shared_runners_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
@@ -222,6 +226,7 @@ module API
:builds_enabled,
:wiki_enabled,
:snippets_enabled,
+ :container_registry_enabled,
:shared_runners_enabled,
:public,
:visibility_level,
@@ -336,7 +341,6 @@ module API
else
not_found!("Source Project")
end
-
end
# Remove a forked_from relationship
@@ -413,7 +417,6 @@ module API
present paginate(projects), with: Entities::Project
end
-
# Get a users list
#
# Example Request:
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 62161aadb9a..f55aceed92c 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -56,8 +56,7 @@ module API
blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
not_found! "File" unless blob
- content_type 'text/plain'
- header *Gitlab::Workhorse.send_git_blob(repo, blob)
+ send_git_blob repo, blob
end
# Get a raw blob contents by blob sha
@@ -80,10 +79,7 @@ module API
not_found! 'Blob' unless blob
- env['api.format'] = :txt
-
- content_type blob.mime_type
- header *Gitlab::Workhorse.send_git_blob(repo, blob)
+ send_git_blob repo, blob
end
# Get a an archive of the repository
@@ -98,7 +94,7 @@ module API
authorize! :download_code, user_project
begin
- header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
not_found!('File')
end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 8ec91485b26..ecc8f2fc5a2 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id])
authenticate_update_runner!(runner)
- attrs = attributes_for_keys [:description, :active, :tag_list]
+ attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user
else
@@ -96,9 +96,14 @@ module API
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
- Ci::RunnerProject.create(runner: runner, project: user_project)
- present runner, with: Entities::Runner
+ runner_project = runner.assign_to(user_project)
+
+ if runner_project.persisted?
+ present runner, with: Entities::Runner
+ else
+ conflict!("Runner was already enabled for this project")
+ end
end
# Disable project's runner
@@ -163,6 +168,7 @@ module API
def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner is locked") if runner.locked?
return if current_user.is_admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 203f04a6259..fc8598daa32 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -4,7 +4,6 @@ module API
before { authenticate! }
before { authorize_admin_project }
-
resource :projects do
# Set <service_slug> service for project
#
diff --git a/lib/api/session.rb b/lib/api/session.rb
index cc646895914..56c202f1294 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -11,8 +11,7 @@ module API
# Example Request:
# POST /session
post "/session" do
- auth = Gitlab::Auth.new
- user = auth.find(params[:email] || params[:login], params[:password])
+ user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
return unauthorized! unless user
present user, with: Entities::UserLogin
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
new file mode 100644
index 00000000000..d3d6827dc54
--- /dev/null
+++ b/lib/api/sidekiq_metrics.rb
@@ -0,0 +1,90 @@
+require 'sidekiq/api'
+
+module API
+ class SidekiqMetrics < Grape::API
+ before { authenticated_as_admin! }
+
+ helpers do
+ def queue_metrics
+ Sidekiq::Queue.all.each_with_object({}) do |queue, hash|
+ hash[queue.name] = {
+ backlog: queue.size,
+ latency: queue.latency.to_i
+ }
+ end
+ end
+
+ def process_metrics
+ Sidekiq::ProcessSet.new.map do |process|
+ {
+ hostname: process['hostname'],
+ pid: process['pid'],
+ tag: process['tag'],
+ started_at: Time.at(process['started_at']),
+ queues: process['queues'],
+ labels: process['labels'],
+ concurrency: process['concurrency'],
+ busy: process['busy']
+ }
+ end
+ end
+
+ def job_stats
+ stats = Sidekiq::Stats.new
+ {
+ processed: stats.processed,
+ failed: stats.failed,
+ enqueued: stats.enqueued
+ }
+ end
+ end
+
+ # Get Sidekiq Queue metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/queue_metrics
+ #
+ get 'sidekiq/queue_metrics' do
+ { queues: queue_metrics }
+ end
+
+ # Get Sidekiq Process metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/process_metrics
+ #
+ get 'sidekiq/process_metrics' do
+ { processes: process_metrics }
+ end
+
+ # Get Sidekiq Job statistics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/job_stats
+ #
+ get 'sidekiq/job_stats' do
+ { jobs: job_stats }
+ end
+
+ # Get Sidekiq Compound metrics. Includes all previous metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/compound_metrics
+ #
+ get 'sidekiq/compound_metrics' do
+ { queues: queue_metrics, processes: process_metrics, jobs: job_stats }
+ end
+ end
+end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
new file mode 100644
index 00000000000..c49e2a21b82
--- /dev/null
+++ b/lib/api/subscriptions.rb
@@ -0,0 +1,60 @@
+module API
+ class Subscriptions < Grape::API
+ before { authenticate! }
+
+ subscribable_types = {
+ 'merge_request' => proc { |id| user_project.merge_requests.find(id) },
+ 'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
+ 'issues' => proc { |id| find_project_issue(id) },
+ 'labels' => proc { |id| find_project_label(id) },
+ }
+
+ resource :projects do
+ subscribable_types.each do |type, finder|
+ type_singularized = type.singularize
+ type_id_str = :"#{type_singularized}_id"
+ entity_class = Entities.const_get(type_singularized.camelcase)
+
+ # Subscribe to a resource
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # subscribable_id (required) - The ID of a resource
+ # Example Request:
+ # POST /projects/:id/labels/:subscribable_id/subscription
+ # POST /projects/:id/issues/:subscribable_id/subscription
+ # POST /projects/:id/merge_requests/:subscribable_id/subscription
+ post ":id/#{type}/:#{type_id_str}/subscription" do
+ resource = instance_exec(params[type_id_str], &finder)
+
+ if resource.subscribed?(current_user)
+ not_modified!
+ else
+ resource.subscribe(current_user)
+ present resource, with: entity_class, current_user: current_user
+ end
+ end
+
+ # Unsubscribe from a resource
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # subscribable_id (required) - The ID of a resource
+ # Example Request:
+ # DELETE /projects/:id/labels/:subscribable_id/subscription
+ # DELETE /projects/:id/issues/:subscribable_id/subscription
+ # DELETE /projects/:id/merge_requests/:subscribable_id/subscription
+ delete ":id/#{type}/:#{type_id_str}/subscription" do
+ resource = instance_exec(params[type_id_str], &finder)
+
+ if !resource.subscribed?(current_user)
+ not_modified!
+ else
+ resource.unsubscribe(current_user)
+ present resource, with: entity_class, current_user: current_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 3e1ed3fe5c7..7b675e05fbb 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -61,7 +61,7 @@ module API
# 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
+ delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
result = DeleteTagService.new(user_project, current_user).
execute(params[:tag_name])
@@ -83,7 +83,7 @@ module API
# description (required) - Release notes with markdown support
# Example Request:
# POST /projects/:id/repository/tags/:tag_name/release
- post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do
+ post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
required_attributes! [:description]
result = CreateReleaseService.new(user_project, current_user).
@@ -104,7 +104,7 @@ module API
# description (required) - Release notes with markdown support
# Example Request:
# PUT /projects/:id/repository/tags/:tag_name/release
- put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do
+ put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
required_attributes! [:description]
result = UpdateReleaseService.new(user_project, current_user).
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
new file mode 100644
index 00000000000..18408797756
--- /dev/null
+++ b/lib/api/templates.rb
@@ -0,0 +1,36 @@
+module API
+ class Templates < Grape::API
+ TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::Gitignore,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ }.freeze
+
+ TEMPLATE_TYPES.each do |template, klass|
+ # Get the list of the available template
+ #
+ # Example Request:
+ # GET /gitignores
+ # GET /gitlab_ci_ymls
+ get template.to_s do
+ present klass.all, with: Entities::TemplatesList
+ end
+
+ # Get the text for a specific template
+ #
+ # Parameters:
+ # name (required) - The name of a template
+ #
+ # Example Request:
+ # GET /gitignores/Elixir
+ # GET /gitlab_ci_ymls/Ruby
+ get "#{template}/:name" do
+ required_attributes! [:name]
+
+ new_template = klass.find(params[:name])
+ not_found!(template.to_s.singularize) unless new_template
+
+ present new_template, with: Entities::Template
+ end
+ end
+ end
+end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
new file mode 100644
index 00000000000..2a6bfa98ca4
--- /dev/null
+++ b/lib/api/todos.rb
@@ -0,0 +1,82 @@
+module API
+ # Todos API
+ class Todos < Grape::API
+ before { authenticate! }
+
+ ISSUABLE_TYPES = {
+ 'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
+ 'issues' => ->(id) { find_project_issue(id) }
+ }
+
+ resource :projects do
+ ISSUABLE_TYPES.each do |type, finder|
+ type_id_str = "#{type.singularize}_id".to_sym
+
+ # Create a todo on an issuable
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # issuable_id (required) - The ID of an issuable
+ # Example Request:
+ # POST /projects/:id/issues/:issuable_id/todo
+ # POST /projects/:id/merge_requests/:issuable_id/todo
+ post ":id/#{type}/:#{type_id_str}/todo" do
+ issuable = instance_exec(params[type_id_str], &finder)
+ todo = TodoService.new.mark_todo(issuable, current_user).first
+
+ if todo
+ present todo, with: Entities::Todo, current_user: current_user
+ else
+ not_modified!
+ end
+ end
+ end
+ end
+
+ resource :todos do
+ helpers do
+ def find_todos
+ TodosFinder.new(current_user, params).execute
+ end
+ end
+
+ # Get a todo list
+ #
+ # Example Request:
+ # GET /todos
+ #
+ get do
+ todos = find_todos
+
+ present paginate(todos), with: Entities::Todo, current_user: current_user
+ end
+
+ # Mark a todo as done
+ #
+ # Parameters:
+ # id: (required) - The ID of the todo being marked as done
+ #
+ # Example Request:
+ # DELETE /todos/:id
+ #
+ delete ':id' do
+ todo = current_user.todos.find(params[:id])
+ todo.done
+
+ present todo, with: Entities::Todo, current_user: current_user
+ end
+
+ # Mark all todos as done
+ #
+ # Example Request:
+ # DELETE /todos
+ #
+ delete do
+ todos = find_todos
+ todos.each(&:done)
+
+ present paginate(Kaminari.paginate_array(todos)), with: Entities::Todo, current_user: current_user
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index ea6fa2dc8a8..8a376d3c2a3 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -76,7 +76,7 @@ module API
required_attributes! [:email, :password, :name, :username]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
- confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
+ 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
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
deleted file mode 100644
index b1aecc2e671..00000000000
--- a/lib/award_emoji.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-class AwardEmoji
- CATEGORIES = {
- other: "Other",
- objects: "Objects",
- places: "Places",
- travel_places: "Travel",
- emoticons: "Emoticons",
- objects_symbols: "Symbols",
- nature: "Nature",
- celebration: "Celebration",
- people: "People",
- activity: "Activity",
- flags: "Flags",
- food_drink: "Food"
- }.with_indifferent_access
-
- CATEGORY_ALIASES = {
- symbols: "objects_symbols",
- foods: "food_drink",
- travel: "travel_places"
- }.with_indifferent_access
-
- def self.normilize_emoji_name(name)
- aliases[name] || name
- end
-
- def self.emoji_by_category
- unless @emoji_by_category
- @emoji_by_category = Hash.new { |h, key| h[key] = [] }
-
- emojis.each do |emoji_name, data|
- data["name"] = emoji_name
-
- # Skip Fitzpatrick(tone) modifiers
- next if data["category"] == "modifier"
-
- category = CATEGORY_ALIASES[data["category"]] || data["category"]
-
- @emoji_by_category[category] << data
- end
-
- @emoji_by_category = @emoji_by_category.sort.to_h
- end
-
- @emoji_by_category
- end
-
- def self.emojis
- @emojis ||= begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
- JSON.parse(File.read(json_path))
- end
- end
-
- def self.unicode
- @unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!)
- end
-
- def self.aliases
- @aliases ||= begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
- JSON.parse(File.read(json_path))
- end
- end
-
- # Returns an Array of Emoji names and their asset URLs.
- def self.urls
- @urls ||= begin
- path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
- prefix = Gitlab::Application.config.assets.prefix
- digest = Gitlab::Application.config.assets.digest
-
- JSON.parse(File.read(path)).map do |hash|
- if digest
- fname = "#{hash['unicode']}-#{hash['digest']}"
- else
- fname = hash['unicode']
- end
-
- { name: hash['name'], path: "#{prefix}/#{fname}.png" }
- end
- end
- end
-end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 67b2a64bd10..22319ec6623 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -86,9 +86,9 @@ module Backup
def report_success(success)
if success
- $progress.puts '[DONE]'.green
+ $progress.puts '[DONE]'.color(:green)
else
- $progress.puts '[FAILED]'.red
+ $progress.puts '[FAILED]'.color(:red)
end
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 4962f5e53ce..2ff3e3bdfb0 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,5 +1,8 @@
module Backup
class Manager
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
+ FOLDERS_TO_BACKUP = %w[repositories db]
+
def pack
# Make sure there is a connection
ActiveRecord::Base.connection.reconnect!
@@ -24,9 +27,9 @@ module Backup
# Set file permissions on open to prevent chmod races.
tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
else
- puts "creating archive #{tar_file} failed".red
+ puts "creating archive #{tar_file} failed".color(:red)
abort 'Backup failed'
end
@@ -35,24 +38,22 @@ module Backup
end
def upload(tar_file)
- remote_directory = Gitlab.config.backup.upload.remote_directory
$progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
connection_settings = Gitlab.config.backup.upload.connection
if connection_settings.blank?
- $progress.puts "skipped".yellow
+ $progress.puts "skipped".color(:yellow)
return
end
- connection = ::Fog::Storage.new(connection_settings)
- directory = connection.directories.get(remote_directory)
+ directory = connect_to_remote_directory(connection_settings)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption)
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
else
- puts "uploading backup to #{remote_directory} failed".red
+ puts "uploading backup to #{remote_directory} failed".color(:red)
abort 'Backup failed'
end
end
@@ -64,9 +65,9 @@ module Backup
next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
else
- puts "deleting tmp directory '#{dir}' failed".red
+ puts "deleting tmp directory '#{dir}' failed".color(:red)
abort 'Backup failed'
end
end
@@ -92,9 +93,9 @@ module Backup
end
end
- $progress.puts "done. (#{removed} removed)".green
+ $progress.puts "done. (#{removed} removed)".color(:green)
else
- $progress.puts "skipping".yellow
+ $progress.puts "skipping".color(:yellow)
end
end
@@ -121,20 +122,20 @@ module Backup
$progress.print "Unpacking backup ... "
unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".red
+ puts "unpacking backup failed".color(:red)
exit 1
else
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
- puts "GitLab version mismatch:".red
- puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red
- puts " Please switch to the following version and try again:".red
- puts " version: #{settings[:gitlab_version]}".red
+ puts "GitLab version mismatch:".color(:red)
+ puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
+ puts " Please switch to the following version and try again:".color(:red)
+ puts " version: #{settings[:gitlab_version]}".color(:red)
puts
puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
@@ -147,21 +148,44 @@ module Backup
end
def skipped?(item)
- settings[:skipped] && settings[:skipped].include?(item)
+ settings[:skipped] && settings[:skipped].include?(item) || disabled_features.include?(item)
end
private
+ def connect_to_remote_directory(connection_settings)
+ connection = ::Fog::Storage.new(connection_settings)
+
+ # We only attempt to create the directory for local backups. For AWS
+ # and other cloud providers, we cannot guarantee the user will have
+ # permission to create the bucket.
+ if connection.service == ::Fog::Storage::Local
+ connection.directories.create(key: remote_directory)
+ else
+ connection.directories.get(remote_directory)
+ end
+ end
+
+ def remote_directory
+ Gitlab.config.backup.upload.remote_directory
+ end
+
def backup_contents
folders_to_backup + archives_to_backup + ["backup_information.yml"]
end
def archives_to_backup
- %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
+ ARCHIVES_TO_BACKUP.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
end
def folders_to_backup
- %w{repositories db}.reject{ |name| skipped?(name) }
+ FOLDERS_TO_BACKUP.reject{ |name| skipped?(name) }
+ end
+
+ def disabled_features
+ features = []
+ features << 'registry' unless Gitlab.config.registry.enabled
+ features
end
def settings
diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb
new file mode 100644
index 00000000000..67fe0231087
--- /dev/null
+++ b/lib/backup/registry.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Registry < Files
+ def initialize
+ super('registry', Settings.registry.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index a82a7e1f7bf..b9773f98d75 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,8 +2,6 @@ require 'yaml'
module Backup
class Repository
- attr_reader :repos_path
-
def dump
prepare
@@ -14,14 +12,14 @@ module Backup
FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
if project.empty_repo?
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
- $progress.puts "[DONE]".green
+ $progress.puts "[DONE]".color(:green)
else
- puts "[FAILED]".red
+ puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
puts output
abort 'Backup failed'
@@ -33,14 +31,14 @@ module Backup
if File.exists?(path_to_repo(wiki))
$progress.print " * #{wiki.path_with_namespace} ... "
if wiki.repository.empty?
- $progress.puts " [SKIPPED]".cyan
+ $progress.puts " [SKIPPED]".color(:cyan)
else
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
- $progress.puts " [DONE]".green
+ $progress.puts " [DONE]".color(:green)
else
- puts " [FAILED]".red
+ puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Backup failed'
end
@@ -50,10 +48,12 @@ module Backup
end
def restore
- if File.exists?(repos_path)
+ Gitlab.config.repositories.storages.each do |name, path|
+ next unless File.exists?(path)
+
# Move repos dir to 'repositories.old' dir
- bk_repos_path = File.join(repos_path, '..', 'repositories.old.' + Time.now.to_i.to_s)
- FileUtils.mv(repos_path, bk_repos_path)
+ bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
+ FileUtils.mv(path, bk_repos_path)
end
FileUtils.mkdir_p(repos_path)
@@ -61,7 +61,7 @@ module Backup
Project.find_each(batch_size: 1000) do |project|
$progress.print " * #{project.path_with_namespace} ... "
- project.namespace.ensure_dir_exist if project.namespace
+ project.ensure_dir_exist
if File.exists?(path_to_bundle(project))
FileUtils.mkdir_p(path_to_repo(project))
@@ -71,9 +71,9 @@ module Backup
end
if system(*cmd, silent)
- $progress.puts "[DONE]".green
+ $progress.puts "[DONE]".color(:green)
else
- puts "[FAILED]".red
+ puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Restore failed'
end
@@ -90,21 +90,21 @@ module Backup
cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
if system(*cmd, silent)
- $progress.puts " [DONE]".green
+ $progress.puts " [DONE]".color(:green)
else
- puts " [FAILED]".red
+ puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Restore failed'
end
end
end
- $progress.print 'Put GitLab hooks in repositories dirs'.yellow
- cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks"
- if system(cmd)
- $progress.puts " [DONE]".green
+ $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
+ cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args
+ if system(*cmd)
+ $progress.puts " [DONE]".color(:green)
else
- puts " [FAILED]".red
+ puts " [FAILED]".color(:red)
puts "failed: #{cmd}"
end
@@ -120,10 +120,6 @@ module Backup
File.join(backup_repos_path, project.path_with_namespace + ".bundle")
end
- def repos_path
- Gitlab.config.gitlab_shell.repos_path
- end
-
def backup_repos_path
File.join(Gitlab.config.backup.path, "repositories")
end
@@ -139,5 +135,11 @@ module Backup
def silent
{err: '/dev/null', out: '/dev/null'}
end
+
+ private
+
+ def repository_storage_paths_args
+ Gitlab.config.repositories.storages.values
+ end
end
end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index b467413a7dd..093382261ae 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -7,10 +7,6 @@ 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/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index b8962379cb5..d77a5e3ff09 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -18,10 +18,6 @@ module Banzai
@object_sym ||= object_name.to_sym
end
- def self.data_reference
- @data_reference ||= "data-#{object_name.dasherize}"
- end
-
def self.object_class_title
@object_title ||= object_class.name.titleize
end
@@ -45,10 +41,6 @@ module Banzai
end
end
- def self.referenced_by(node)
- { object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
- end
-
def object_class
self.class.object_class
end
@@ -111,7 +103,7 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
- each_node do |node|
+ nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
@@ -168,11 +160,7 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attribute(
- original: link_text || match,
- project: project.id,
- object_sym => object.id
- )
+ data = data_attributes_for(link_text || match, project, object)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
@@ -191,6 +179,14 @@ module Banzai
end
end
+ def data_attributes_for(text, project, object)
+ data_attribute(
+ original: text,
+ project: project.id,
+ object_sym => object.id
+ )
+ end
+
def object_link_text_extras(object, matches)
extras = []
@@ -214,6 +210,56 @@ module Banzai
text
end
+ # Returns a Hash containing all object references (e.g. issue IDs) per the
+ # project they belong to.
+ def references_per_project
+ @references_per_project ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+
+ regex = Regexp.union(object_class.reference_pattern,
+ object_class.link_reference_pattern)
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ project = $~[:project] || current_project_path
+ symbol = $~[object_sym]
+
+ refs[project] << symbol if object_class.reference_valid?(symbol)
+ end
+ end
+
+ refs
+ end
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def projects_per_reference
+ @projects_per_reference ||= begin
+ hash = {}
+ refs = Set.new
+
+ references_per_project.each do |project_ref, _|
+ refs << project_ref
+ end
+
+ find_projects_for_paths(refs.to_a).each do |project|
+ hash[project.path_with_namespace] = project
+ end
+
+ hash
+ end
+ end
+
+ # Returns the projects for the given paths.
+ def find_projects_for_paths(paths)
+ Project.where_paths_in(paths).includes(:namespace)
+ end
+
+ def current_project_path
+ @current_project_path ||= project.path_with_namespace
+ end
+
private
def project_refs_cache
@@ -236,7 +282,9 @@ module Banzai
if cache.key?(key)
cache[key]
else
- cache[key] = yield
+ value = yield
+ cache[key] = value if key.present?
+ value
end
end
end
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index b469ea0f626..bbb88c979cc 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
#
# This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit_range
+
def self.object_class
CommitRange
end
@@ -14,34 +16,18 @@ module Banzai
end
end
- def self.referenced_by(node)
- project = Project.find(node.attr("data-project")) rescue nil
- return unless project
-
- id = node.attr("data-commit-range")
- range = find_object(project, id)
-
- return unless range
-
- { commit_range: range }
- end
-
def initialize(*args)
super
@commit_map = {}
end
- def self.find_object(project, id)
+ def find_object(project, id)
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
end
- def find_object(*args)
- self.class.find_object(*args)
- end
-
def url_for_object(range, project)
h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project,
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index bd88207326c..2ce1816672b 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
#
# This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit
+
def self.object_class
Commit
end
@@ -14,28 +16,12 @@ module Banzai
end
end
- def self.referenced_by(node)
- project = Project.find(node.attr("data-project")) rescue nil
- return unless project
-
- id = node.attr("data-commit")
- commit = find_object(project, id)
-
- return unless commit
-
- { commit: commit }
- end
-
- def self.find_object(project, id)
+ def find_object(project, id)
if project && project.valid_repo?
project.commit(id)
end
end
- def find_object(*args)
- self.class.find_object(*args)
- end
-
def url_for_object(commit, project)
h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index d25de900674..ae7d31cf191 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -61,7 +61,7 @@ module Banzai
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
- @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
end
def emoji_pattern
@@ -69,7 +69,7 @@ module Banzai
end
def emoji_filename(name)
- "#{Emoji.emoji_filename(name)}.png"
+ "#{Gitlab::Emoji.emoji_filename(name)}.png"
end
end
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 37344b90576..eaa702952cc 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
# References are ignored if the project doesn't use an external issue
# tracker.
class ExternalIssueReferenceFilter < ReferenceFilter
+ self.reference_type = :external_issue
+
# Public: Find `JIRA-123` issue references in text
#
# ExternalIssueReferenceFilter.references_in(text) do |match, issue|
@@ -21,18 +23,6 @@ module Banzai
end
end
- def self.referenced_by(node)
- project = Project.find(node.attr("data-project")) rescue nil
- return unless project
-
- id = node.attr("data-external-issue")
- external_issue = ExternalIssue.new(id, project)
-
- return unless external_issue
-
- { external_issue: external_issue }
- end
-
def call
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 38c4219518e..0a29c547a4d 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,18 +3,10 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
- doc.search('a').each do |node|
- link = node.attr('href')
-
- next unless link
-
- # Skip non-HTTP(S) links
- next unless link.start_with?('http')
-
- # Skip internal links
- next if link.start_with?(internal_url)
-
+ # Skip non-HTTP(S) links and internal links
+ doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
node.set_attribute('rel', 'nofollow noreferrer')
+ node.set_attribute('target', '_blank')
end
doc
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index ccd106860bd..f0fb6084a35 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -8,6 +8,10 @@ module Banzai
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
+ div = doc.document.create_element(
+ 'div',
+ class: 'image-container'
+ )
link = doc.document.create_element(
'a',
@@ -17,7 +21,10 @@ module Banzai
)
link.children = img.clone
- img.replace(link)
+
+ div.children = link
+
+ img.replace(div)
end
doc
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
new file mode 100644
index 00000000000..beb21b19ab3
--- /dev/null
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -0,0 +1,26 @@
+module Banzai
+ module Filter
+ class InlineDiffFilter < HTML::Pipeline::Filter
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+ def call
+ search_text_nodes(doc).each do |node|
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ content = node.to_html
+ html_content = inline_diff_filter(content)
+
+ next if content == html_content
+
+ node.replace(html_content)
+ end
+ doc
+ end
+
+ def inline_diff_filter(text)
+ html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>')
+ html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>')
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 59c5e89c546..4042e9a4c25 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -5,25 +5,70 @@ module Banzai
#
# This filter supports cross-project references.
class IssueReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :issue
+
def self.object_class
Issue
end
- def self.user_can_see_reference?(user, node, context)
- # It is not possible to check access rights for external issue trackers
- return true if context[:project].try(:external_issue_tracker)
-
- 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)
+ def find_object(project, iid)
+ issues_per_project[project][iid]
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
end
+
+ def project_from_ref(ref)
+ projects_per_reference[ref || current_project_path]
+ end
+
+ # Returns a Hash containing the issues per Project instance.
+ def issues_per_project
+ @issues_per_project ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ projects_per_reference.each do |path, project|
+ issue_ids = references_per_project[path]
+
+ if project.default_issues_tracker?
+ issues = project.issues.where(iid: issue_ids.to_a)
+ else
+ issues = issue_ids.map { |id| ExternalIssue.new(id, project) }
+ end
+
+ issues.each do |issue|
+ hash[project][issue.iid.to_i] = issue
+ end
+ end
+
+ hash
+ end
+ end
+
+ def object_link_title(object)
+ if object.is_a?(ExternalIssue)
+ "Issue in #{object.project.external_issue_tracker.title}"
+ else
+ super
+ end
+ end
+
+ def data_attributes_for(text, project, object)
+ if object.is_a?(ExternalIssue)
+ data_attribute(
+ project: project.id,
+ external_issue: object.id,
+ reference_type: ExternalIssueReferenceFilter.reference_type
+ )
+ else
+ super
+ end
+ end
+
+ def find_projects_for_paths(paths)
+ super(paths).includes(:gitlab_issue_tracker_service)
+ end
end
end
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 8488a493b55..e258dc8e2bf 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -2,6 +2,8 @@ module Banzai
module Filter
# HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :label
+
def self.object_class
Label
end
@@ -11,13 +13,13 @@ module Banzai
end
def self.references_in(text, pattern = Label.reference_pattern)
- text.gsub(pattern) do |match|
+ unescape_html_entities(text).gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
end
end
def references_in(text, pattern = Label.reference_pattern)
- text.gsub(pattern) do |match|
+ unescape_html_entities(text).gsub(pattern) do |match|
label = find_label($~[:project], $~[:label_id], $~[:label_name])
if label
@@ -64,6 +66,10 @@ module Banzai
LabelsHelper.render_colored_cross_project_label(object)
end
end
+
+ def unescape_html_entities(text)
+ CGI.unescapeHTML(text.to_s)
+ end
end
end
end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index cad38a51851..ac5216d9cfb 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -5,6 +5,8 @@ module Banzai
#
# This filter supports cross-project references.
class MergeRequestReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :merge_request
+
def self.object_class
MergeRequest
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 4cb82178024..ca686c87d97 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -2,6 +2,8 @@ module Banzai
module Filter
# HTML filter that replaces milestone references with links.
class MilestoneReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :milestone
+
def self.object_class
Milestone
end
@@ -10,11 +12,53 @@ module Banzai
project.milestones.find_by(iid: id)
end
- def url_for_object(issue, project)
+ def references_in(text, pattern = Milestone.reference_pattern)
+ # We'll handle here the references that follow the `reference_pattern`.
+ # Other patterns (for example, the link pattern) are handled by the
+ # default implementation.
+ return super(text, pattern) if pattern != Milestone.reference_pattern
+
+ text.gsub(pattern) do |match|
+ milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name])
+
+ if milestone
+ yield match, milestone.iid, $~[:project], $~
+ else
+ match
+ end
+ end
+ end
+
+ def find_milestone(project_ref, milestone_id, milestone_name)
+ project = project_from_ref(project_ref)
+ return unless project
+
+ milestone_params = milestone_params(milestone_id, milestone_name)
+ project.milestones.find_by(milestone_params)
+ end
+
+ def milestone_params(iid, name)
+ if name
+ { name: name.tr('"', '') }
+ else
+ { iid: iid.to_i }
+ end
+ end
+
+ def url_for_object(milestone, project)
h = Gitlab::Routing.url_helpers
h.namespace_project_milestone_url(project.namespace, project, milestone,
only_path: context[:only_path])
end
+
+ def object_link_text(object, matches)
+ if context[:project] == object.project
+ super
+ else
+ "#{escape_once(super)} <i>in #{escape_once(object.project.path_with_namespace)}</i>".
+ html_safe
+ end
+ end
end
end
end
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index e589b5df6ec..c59a80dd1c7 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -7,34 +7,20 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- 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.
- text = node.attr('data-original') || node.text
- node.replace(text)
- end
- end
+ Redactor.new(project, current_user).redact([doc])
doc
end
private
- def user_can_see_reference?(node)
- if node.has_attribute?('data-reference-filter')
- reference_type = node.attr('data-reference-filter')
- reference_filter = Banzai::Filter.const_get(reference_type)
-
- reference_filter.user_can_see_reference?(current_user, node, context)
- else
- true
- end
- end
-
def current_user
context[:current_user]
end
+
+ def project
+ context[:project]
+ end
end
end
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 31386cf851c..bf058241cda 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -8,24 +8,8 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
- def self.user_can_see_reference?(user, node, context)
- if node.has_attribute?('data-project')
- project_id = node.attr('data-project').to_i
- return true if project_id == context[:project].try(:id)
-
- project = Project.find(project_id) rescue nil
- Ability.abilities.allowed?(user, :read_project, project)
- else
- true
- end
- end
-
- def self.user_can_reference?(user, node, context)
- true
- end
-
- def self.referenced_by(node)
- raise NotImplementedError, "#{self} does not implement #{__method__}"
+ class << self
+ attr_accessor :reference_type
end
# Returns a data attribute String to attach to a reference link
@@ -43,7 +27,9 @@ module Banzai
#
# Returns a String
def data_attribute(attributes = {})
- attributes[:reference_filter] = self.class.name.demodulize
+ attributes = attributes.reject { |_, v| v.nil? }
+
+ attributes[:reference_type] ||= self.class.reference_type
attributes.delete(:original) if context[:no_original_data]
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
@@ -82,6 +68,8 @@ module Banzai
# by `ignore_ancestor_query`. Link tags are not processed if they have a
# "gfm" class or the "href" attribute is empty.
def each_node
+ return to_enum(__method__) unless block_given?
+
query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
| descendant-or-self::a[
not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
@@ -92,6 +80,11 @@ module Banzai
end
end
+ # Returns an Array containing all HTML nodes.
+ def nodes
+ @nodes ||= each_node.to_a
+ end
+
# Yields the link's URL and text whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
deleted file mode 100644
index 96fdb06304e..00000000000
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module Banzai
- module Filter
- # HTML filter that gathers all referenced records that the current user has
- # permission to view.
- #
- # Expected to be run in its own post-processing pipeline.
- #
- class ReferenceGathererFilter < HTML::Pipeline::Filter
- def initialize(*)
- super
-
- result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
- end
-
- def call
- Querying.css(doc, 'a.gfm').each do |node|
- gather_references(node)
- end
-
- load_lazy_references unless ReferenceExtractor.lazy?
-
- doc
- end
-
- private
-
- def gather_references(node)
- return unless node.has_attribute?('data-reference-filter')
-
- reference_type = node.attr('data-reference-filter')
- reference_filter = Banzai::Filter.const_get(reference_type)
-
- return if context[:reference_filter] && reference_filter != context[:reference_filter]
-
- return if author && !reference_filter.user_can_reference?(author, node, context)
-
- return unless reference_filter.user_can_see_reference?(current_user, node, context)
-
- references = reference_filter.referenced_by(node)
- return unless references
-
- references.each do |type, values|
- Array.wrap(values).each do |value|
- result[:references][type] << value
- end
- end
- end
-
- def load_lazy_references
- refs = result[:references]
- refs.each do |type, values|
- refs[type] = ReferenceExtractor.lazily(values)
- end
- end
-
- def current_user
- context[:current_user]
- end
-
- def author
- context[:author]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index ea21c7b041c..c78da404607 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -14,6 +14,8 @@ module Banzai
def call
return doc unless linkable_files?
+ @uri_types = {}
+
doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
end
@@ -48,7 +50,7 @@ module Banzai
uri.path = [
relative_url_root,
context[:project].path_with_namespace,
- path_type(file_path),
+ uri_type(file_path),
ref || context[:project].default_branch, # if no ref exists, point to the default branch
file_path
].compact.join('/').squeeze('/').chomp('/')
@@ -87,7 +89,7 @@ module Banzai
return path unless request_path
parts = request_path.split('/')
- parts.pop if path_type(request_path) != 'tree'
+ parts.pop if uri_type(request_path) != :tree
while path.start_with?('../')
parts.pop
@@ -98,45 +100,20 @@ module Banzai
end
def file_exists?(path)
- return false if path.nil?
- repository.blob_at(current_sha, path).present? ||
- repository.tree(current_sha, path).entries.any?
- end
-
- # Get the type of the given path
- #
- # path - String path to check
- #
- # Examples:
- #
- # path_type('doc/README.md') # => 'blob'
- # path_type('doc/logo.png') # => 'raw'
- # path_type('doc/api') # => 'tree'
- #
- # Returns a String
- def path_type(path)
- unescaped_path = Addressable::URI.unescape(path)
-
- if tree?(unescaped_path)
- 'tree'
- elsif image?(unescaped_path)
- 'raw'
- else
- 'blob'
- end
+ path.present? && !!uri_type(path)
end
- def tree?(path)
- repository.tree(current_sha, path).entries.any?
- end
+ def uri_type(path)
+ @uri_types[path] ||= begin
+ unescaped_path = Addressable::URI.unescape(path)
- def image?(path)
- repository.blob_at(current_sha, path).try(:image?)
+ current_commit.uri_type(unescaped_path)
+ end
end
- def current_sha
- context[:commit].try(:id) ||
- ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
+ def current_commit
+ @current_commit ||= context[:commit] ||
+ ref ? repository.commit(ref) : repository.head_commit
end
def relative_url_root
@@ -148,7 +125,7 @@ module Banzai
end
def repository
- context[:project].try(:repository)
+ @repository ||= context[:project].try(:repository)
end
end
end
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index d507eb5ebe1..212a0bbf2a0 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -5,6 +5,8 @@ module Banzai
#
# This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :snippet
+
def self.object_class
Snippet
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 62a79c62e20..536b478979f 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -27,12 +27,17 @@ module Banzai
highlighted = "<pre>#{code}</pre>"
end
- # Replace the parent `pre` element with the entire highlighted block
- node.parent.replace(highlighted)
+ # Extracted to a method to measure it
+ replace_parent_pre_element(node, highlighted)
end
private
+ def replace_parent_pre_element(node, highlighted)
+ # Replace the parent `pre` element with the entire highlighted block
+ node.parent.replace(highlighted)
+ end
+
# Override Rouge::Plugins::Redcarpet#rouge_formatter
def rouge_formatter(lexer)
Rouge::Formatters::HTMLGitlab.new(
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index c0f503c9af3..45bb66dc99f 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -10,11 +10,11 @@ module Banzai
def call
return doc unless project
- doc.search('a').each do |el|
+ doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
process_link_attr el.attribute('href')
end
- doc.search('img').each do |el|
+ doc.xpath('descendant-or-self::img[starts-with(@src, "/uploads/")]').each do |el|
process_link_attr el.attribute('src')
end
@@ -24,12 +24,7 @@ module Banzai
protected
def process_link_attr(html_attr)
- return if html_attr.blank?
-
- uri = html_attr.value
- if uri.starts_with?("/uploads/")
- html_attr.value = build_url(uri).to_s
- end
+ html_attr.value = build_url(html_attr.value).to_s
end
def build_url(uri)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index eea3af842b6..5b0a6d8541b 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
#
# A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter
+ self.reference_type = :user
+
# Public: Find `@user` user references in text
#
# UserReferenceFilter.references_in(text) do |match, username|
@@ -21,50 +23,13 @@ module Banzai
end
end
- def self.referenced_by(node)
- if node.has_attribute?('data-group')
- group = Group.find(node.attr('data-group')) rescue nil
- return unless group
-
- { user: group.users }
- elsif node.has_attribute?('data-user')
- { user: LazyReference.new(User, node.attr('data-user')) }
- elsif node.has_attribute?('data-project')
- project = Project.find(node.attr('data-project')) rescue nil
- return unless project
-
- { user: project.team.members.flatten }
- end
- end
-
- def self.user_can_see_reference?(user, node, context)
- if node.has_attribute?('data-group')
- group = Group.find(node.attr('data-group')) rescue nil
- Ability.abilities.allowed?(user, :read_group, group)
- else
- super
- end
- end
-
- def self.user_can_reference?(user, node, context)
- # Only team members can reference `@all`
- if node.has_attribute?('data-project')
- project = Project.find(node.attr('data-project')) rescue nil
- return false unless project
-
- user && project.team.member?(user)
- else
- super
- end
- end
-
def call
return doc if project.nil?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
- each_node do |node|
+ nodes.each do |node|
if text_node?(node)
replace_text_when_pattern_matches(node, ref_pattern) do |content|
user_link_filter(content)
@@ -94,7 +59,7 @@ module Banzai
self.class.references_in(text) do |match, username|
if username == 'all'
link_to_all(link_text: link_text)
- elsif namespace = Namespace.find_by(path: username)
+ elsif namespace = namespaces[username]
link_to_namespace(namespace, link_text: link_text) || match
else
match
@@ -102,6 +67,31 @@ module Banzai
end
end
+ # Returns a Hash containing all Namespace objects for the username
+ # references in the current document.
+ #
+ # The keys of this Hash are the namespace paths, the values the
+ # corresponding Namespace objects.
+ def namespaces
+ @namespaces ||=
+ Namespace.where(path: usernames).each_with_object({}) do |row, hash|
+ hash[row.path] = row
+ end
+ end
+
+ # Returns all usernames referenced in the current document.
+ def usernames
+ refs = Set.new
+
+ nodes.each do |node|
+ node.to_html.scan(User.reference_pattern) do
+ refs << $~[:user]
+ end
+ end
+
+ refs.to_a
+ end
+
private
def urls
@@ -114,9 +104,12 @@ module Banzai
def link_to_all(link_text: nil)
project = context[:project]
+ author = context[:author]
+
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
- data = data_attribute(project: project.id)
+
+ data = data_attribute(project: project.id, author: author.try(:id))
text = link_text || User.reference_prefix + 'all'
link_tag(url, data, text)
diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
index 06d10c98501..269d5bf74fa 100644
--- a/lib/banzai/filter/wiki_link_filter.rb
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -2,12 +2,12 @@ require 'uri'
module Banzai
module Filter
- # HTML filter that "fixes" relative links to files in a repository.
+ # HTML filter that "fixes" links to pages/files in a wiki.
+ # Rewrite rules are documented in the `WikiPipeline` spec.
#
# Context options:
# :project_wiki
class WikiLinkFilter < HTML::Pipeline::Filter
-
def call
return doc unless project_wiki?
@@ -25,31 +25,15 @@ module Banzai
end
def process_link_attr(html_attr)
- return if html_attr.blank? || file_reference?(html_attr)
+ return if html_attr.blank?
- uri = URI(html_attr.value)
- if uri.relative? && uri.path.present?
- html_attr.value = rebuild_wiki_uri(uri).to_s
- end
- rescue URI::Error
+ html_attr.value = apply_rewrite_rules(html_attr.value)
+ rescue URI::Error, Addressable::URI::InvalidURIError
# noop
end
- def rebuild_wiki_uri(uri)
- uri.path = ::File.join(project_wiki_base_path, uri.path)
- uri
- end
-
- def file_reference?(html_attr)
- !File.extname(html_attr.value).blank?
- end
-
- def project_wiki
- context[:project_wiki]
- end
-
- def project_wiki_base_path
- project_wiki && project_wiki.wiki_base_path
+ def apply_rewrite_rules(link_string)
+ Rewriter.new(link_string, wiki: context[:project_wiki], slug: context[:page_slug]).apply_rules
end
end
end
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
new file mode 100644
index 00000000000..2e2c8da311e
--- /dev/null
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -0,0 +1,40 @@
+module Banzai
+ module Filter
+ class WikiLinkFilter < HTML::Pipeline::Filter
+ class Rewriter
+ def initialize(link_string, wiki:, slug:)
+ @uri = Addressable::URI.parse(link_string)
+ @wiki_base_path = wiki && wiki.wiki_base_path
+ @slug = slug
+ end
+
+ def apply_rules
+ apply_file_link_rules!
+ apply_hierarchical_link_rules!
+ apply_relative_link_rules!
+ @uri.to_s
+ end
+
+ private
+
+ # Of the form 'file.md'
+ def apply_file_link_rules!
+ @uri = Addressable::URI.join(@slug, @uri) if @uri.extname.present?
+ end
+
+ # Of the form `./link`, `../link`, or similar
+ def apply_hierarchical_link_rules!
+ @uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
+ end
+
+ # Any link _not_ of the form `http://example.com/`
+ def apply_relative_link_rules!
+ if @uri.relative? && @uri.path.present?
+ link = ::File.join(@wiki_base_path, @uri.path)
+ @uri = Addressable::URI.parse(link)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb
deleted file mode 100644
index 1095b4debc7..00000000000
--- a/lib/banzai/lazy_reference.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Banzai
- class LazyReference
- def self.load(refs)
- lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
-
- lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
- ids = refs.flat_map(&:ids)
- klass.where(id: ids)
- end
-
- values + lazy_values
- end
-
- attr_reader :klass, :ids
-
- def initialize(klass, ids)
- @klass = klass
- @ids = Array.wrap(ids).map(&:to_i)
- end
-
- def load
- self.klass.where(id: self.ids)
- end
- end
-end
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
new file mode 100644
index 00000000000..bab6a9934d1
--- /dev/null
+++ b/lib/banzai/note_renderer.rb
@@ -0,0 +1,22 @@
+module Banzai
+ module NoteRenderer
+ # Renders a collection of Note instances.
+ #
+ # notes - The notes to render.
+ # project - The project to use for rendering/redacting.
+ # user - The user viewing the notes.
+ # path - The request path.
+ # wiki - The project's wiki.
+ # git_ref - The current Git reference.
+ def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil)
+ renderer = ObjectRenderer.new(project,
+ user,
+ requested_path: path,
+ project_wiki: wiki,
+ ref: git_ref,
+ pipeline: :note)
+
+ renderer.render(notes, :note)
+ end
+ end
+end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
new file mode 100644
index 00000000000..f0e4f28bf12
--- /dev/null
+++ b/lib/banzai/object_renderer.rb
@@ -0,0 +1,85 @@
+module Banzai
+ # Class for rendering multiple objects (e.g. Note instances) in a single pass.
+ #
+ # Rendered Markdown is stored in an attribute in every object based on the
+ # name of the attribute containing the Markdown. For example, when the
+ # attribute `note` is rendered the HTML is stored in `note_html`.
+ class ObjectRenderer
+ attr_reader :project, :user
+
+ # Make sure to set the appropriate pipeline in the `raw_context` attribute
+ # (e.g. `:note` for Note instances).
+ #
+ # project - A Project to use for rendering and redacting Markdown.
+ # user - The user viewing the Markdown/HTML documents, if any.
+ # context - A Hash containing extra attributes to use in the rendering
+ # pipeline.
+ def initialize(project, user = nil, raw_context = {})
+ @project = project
+ @user = user
+ @raw_context = raw_context
+ end
+
+ # Renders and redacts an Array of objects.
+ #
+ # objects - The objects to render
+ # attribute - The attribute containing the raw Markdown to render.
+ #
+ # Returns the same input objects.
+ def render(objects, attribute)
+ documents = render_objects(objects, attribute)
+ redacted = redact_documents(documents)
+
+ objects.each_with_index do |object, index|
+ object.__send__("#{attribute}_html=", redacted.fetch(index))
+ end
+
+ objects
+ end
+
+ # Renders the attribute of every given object.
+ def render_objects(objects, attribute)
+ objects.map do |object|
+ render_attribute(object, attribute)
+ end
+ end
+
+ # Redacts the list of documents.
+ #
+ # Returns an Array containing the redacted documents.
+ def redact_documents(documents)
+ redactor = Redactor.new(project, user)
+
+ redactor.redact(documents).map do |document|
+ document.to_html.html_safe
+ end
+ end
+
+ # Returns a Banzai context for the given object and attribute.
+ def context_for(object, attribute)
+ context = base_context.merge(cache_key: [object, attribute])
+
+ if object.respond_to?(:author)
+ context[:author] = object.author
+ end
+
+ context
+ end
+
+ # Renders the attribute of an object.
+ #
+ # Returns a `Nokogiri::HTML::Document`.
+ def render_attribute(object, attribute)
+ context = context_for(object, attribute)
+
+ string = object.__send__(attribute)
+ html = Banzai.render(string, context)
+
+ Banzai::Pipeline[:relative_link].to_document(html, context)
+ end
+
+ def base_context
+ @base_context ||= @raw_context.merge(current_user: user, project: project)
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index f2395867658..042fb2e6e14 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,23 +1,16 @@
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
+ WHITELIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
+ elements: Banzai::Filter::SanitizationFilter::LIMITED[:elements] - %w(pre code img ol ul li)
+ )
+
def self.transform_context(context)
super(context).merge(
# SanitizationFilter
- whitelist: whitelist
+ 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/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb
index d47ddfda4be..3c974f73176 100644
--- a/lib/banzai/pipeline/full_pipeline.rb
+++ b/lib/banzai/pipeline/full_pipeline.rb
@@ -1,7 +1,6 @@
module Banzai
module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
-
end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index ed3cfd6b023..b27ecf3c923 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -23,7 +23,8 @@ module Banzai
Filter::LabelReferenceFilter,
Filter::MilestoneReferenceFilter,
- Filter::TaskListFilter
+ Filter::TaskListFilter,
+ Filter::InlineDiffFilter
]
end
diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/relative_link_pipeline.rb
index 919998380e4..270990e7ab4 100644
--- a/lib/banzai/pipeline/reference_extraction_pipeline.rb
+++ b/lib/banzai/pipeline/relative_link_pipeline.rb
@@ -1,9 +1,9 @@
module Banzai
module Pipeline
- class ReferenceExtractionPipeline < BasePipeline
+ class RelativeLinkPipeline < BasePipeline
def self.filters
FilterArray[
- Filter::ReferenceGathererFilter
+ Filter::RelativeLinkFilter
]
end
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
new file mode 100644
index 00000000000..ffd267d5e9a
--- /dev/null
+++ b/lib/banzai/redactor.rb
@@ -0,0 +1,69 @@
+module Banzai
+ # Class for removing Markdown references a certain user is not allowed to
+ # view.
+ class Redactor
+ attr_reader :user, :project
+
+ # project - A Project to use for redacting links.
+ # user - The currently logged in user (if any).
+ def initialize(project, user = nil)
+ @project = project
+ @user = user
+ end
+
+ # Redacts the references in the given Array of documents.
+ #
+ # This method modifies the given documents in-place.
+ #
+ # documents - A list of HTML documents containing references to redact.
+ #
+ # Returns the documents passed as the first argument.
+ def redact(documents)
+ nodes = documents.flat_map do |document|
+ Querying.css(document, 'a.gfm[data-reference-type]')
+ end
+
+ redact_nodes(nodes)
+
+ documents
+ end
+
+ # Redacts the given nodes
+ #
+ # nodes - An Array of HTML nodes to redact.
+ def redact_nodes(nodes)
+ visible = nodes_visible_to_user(nodes)
+
+ nodes.each do |node|
+ unless visible.include?(node)
+ # The reference should be replaced by the original text,
+ # which is not always the same as the rendered text.
+ text = node.attr('data-original') || node.text
+ node.replace(text)
+ end
+ end
+ end
+
+ # Returns the nodes visible to the current user.
+ #
+ # nodes - The input nodes to check.
+ #
+ # Returns a new Array containing the visible nodes.
+ def nodes_visible_to_user(nodes)
+ per_type = Hash.new { |h, k| h[k] = [] }
+ visible = Set.new
+
+ nodes.each do |node|
+ per_type[node.attr('data-reference-type')] << node
+ end
+
+ per_type.each do |type, nodes|
+ parser = Banzai::ReferenceParser[type].new(project, user)
+
+ visible.merge(parser.nodes_visible_to_user(user, nodes))
+ end
+
+ visible
+ end
+ end
+end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index f4079538ec5..bf366962aef 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -1,28 +1,6 @@
module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
- class << self
- LAZY_KEY = :banzai_reference_extractor_lazy
-
- def lazy?
- Thread.current[LAZY_KEY]
- end
-
- def lazily(values = nil, &block)
- return (values || block.call).uniq if lazy?
-
- begin
- Thread.current[LAZY_KEY] = true
-
- values ||= block.call
-
- Banzai::LazyReference.load(values.uniq).uniq
- ensure
- Thread.current[LAZY_KEY] = false
- end
- end
- end
-
def initialize
@texts = []
end
@@ -31,23 +9,21 @@ module Banzai
@texts << Renderer.render(text, context)
end
- def references(type, context = {})
- filter = Banzai::Filter["#{type}_reference"]
+ def references(type, project, current_user = nil)
+ processor = Banzai::ReferenceParser[type].
+ new(project, current_user)
+
+ processor.process(html_documents)
+ end
- context.merge!(
- pipeline: :reference_extraction,
+ private
- # ReferenceGathererFilter
- reference_filter: filter
- )
+ def html_documents
+ # This ensures that we don't memoize anything until we have a number of
+ # text blobs to parse.
+ return [] if @texts.empty?
- self.class.lazily do
- @texts.flat_map do |html|
- text_context = context.dup
- result = Renderer.render_result(html, text_context)
- result[:references][type]
- end.uniq
- end
+ @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) }
end
end
end
diff --git a/lib/banzai/reference_parser.rb b/lib/banzai/reference_parser.rb
new file mode 100644
index 00000000000..557bec4316e
--- /dev/null
+++ b/lib/banzai/reference_parser.rb
@@ -0,0 +1,14 @@
+module Banzai
+ module ReferenceParser
+ # Returns the reference parser class for the given type
+ #
+ # Example:
+ #
+ # Banzai::ReferenceParser['issue']
+ #
+ # This would return the `Banzai::ReferenceParser::IssueParser` class.
+ def self.[](name)
+ const_get("#{name.to_s.camelize}Parser")
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
new file mode 100644
index 00000000000..3d7b9c4a024
--- /dev/null
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -0,0 +1,204 @@
+module Banzai
+ module ReferenceParser
+ # Base class for reference parsing classes.
+ #
+ # Each parser should also specify its reference type by calling
+ # `self.reference_type = ...` in the body of the class. The value of this
+ # method should be a symbol such as `:issue` or `:merge_request`. For
+ # example:
+ #
+ # class IssueParser < BaseParser
+ # self.reference_type = :issue
+ # end
+ #
+ # The reference type is used to determine what nodes to pass to the
+ # `referenced_by` method.
+ #
+ # Parser classes should either implement the instance method
+ # `references_relation` or overwrite `referenced_by`. The
+ # `references_relation` method is supposed to return an
+ # ActiveRecord::Relation used as a base relation for retrieving the objects
+ # referenced in a set of HTML nodes.
+ #
+ # Each class can implement two additional methods:
+ #
+ # * `nodes_user_can_reference`: returns an Array of nodes the given user can
+ # refer to.
+ # * `nodes_visible_to_user`: returns an Array of nodes that are visible to
+ # the given user.
+ #
+ # You only need to overwrite these methods if you want to tweak who can see
+ # which references. For example, the IssueParser class defines its own
+ # `nodes_visible_to_user` method so it can ensure users can only see issues
+ # they have access to.
+ class BaseParser
+ class << self
+ attr_accessor :reference_type
+ end
+
+ # Returns the attribute name containing the value for every object to be
+ # parsed by the current parser.
+ #
+ # For example, for a parser class that returns "Animal" objects this
+ # attribute would be "data-animal".
+ def self.data_attribute
+ @data_attribute ||= "data-#{reference_type.to_s.dasherize}"
+ end
+
+ def initialize(project = nil, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ # Returns all the nodes containing references that the user can refer to.
+ def nodes_user_can_reference(user, nodes)
+ nodes
+ end
+
+ # Returns all the nodes that are visible to the given user.
+ def nodes_visible_to_user(user, nodes)
+ projects = lazy { projects_for_nodes(nodes) }
+ project_attr = 'data-project'
+
+ nodes.select do |node|
+ if node.has_attribute?(project_attr)
+ node_id = node.attr(project_attr).to_i
+
+ if project && project.id == node_id
+ true
+ else
+ can?(user, :read_project, projects[node_id])
+ end
+ else
+ true
+ end
+ end
+ end
+
+ # Returns an Array of objects referenced by any of the given HTML nodes.
+ def referenced_by(nodes)
+ ids = unique_attribute_values(nodes, self.class.data_attribute)
+
+ references_relation.where(id: ids)
+ end
+
+ # Returns the ActiveRecord::Relation to use for querying references in the
+ # DB.
+ def references_relation
+ raise NotImplementedError,
+ "#{self.class} does not implement #{__method__}"
+ end
+
+ # Returns a Hash containing attribute values per project ID.
+ #
+ # The returned Hash uses the following format:
+ #
+ # { project id => [value1, value2, ...] }
+ #
+ # nodes - An Array of HTML nodes to process.
+ # attribute - The name of the attribute (as a String) for which to gather
+ # values.
+ #
+ # Returns a Hash.
+ def gather_attributes_per_project(nodes, attribute)
+ per_project = Hash.new { |hash, key| hash[key] = Set.new }
+
+ nodes.each do |node|
+ project_id = node.attr('data-project').to_i
+ id = node.attr(attribute)
+
+ per_project[project_id] << id if id
+ end
+
+ per_project
+ end
+
+ # Returns a Hash containing objects for an attribute grouped per their
+ # IDs.
+ #
+ # The returned Hash uses the following format:
+ #
+ # { id value => row }
+ #
+ # nodes - An Array of HTML nodes to process.
+ #
+ # collection - The model or ActiveRecord relation to use for retrieving
+ # rows from the database.
+ #
+ # attribute - The name of the attribute containing the primary key values
+ # for every row.
+ #
+ # Returns a Hash.
+ def grouped_objects_for_nodes(nodes, collection, attribute)
+ return {} if nodes.empty?
+
+ ids = unique_attribute_values(nodes, attribute)
+
+ collection.where(id: ids).each_with_object({}) do |row, hash|
+ hash[row.id] = row
+ end
+ end
+
+ # Returns an Array containing all unique values of an attribute of the
+ # given nodes.
+ def unique_attribute_values(nodes, attribute)
+ values = Set.new
+
+ nodes.each do |node|
+ if node.has_attribute?(attribute)
+ values << node.attr(attribute)
+ end
+ end
+
+ values.to_a
+ end
+
+ # Processes the list of HTML documents and returns an Array containing all
+ # the references.
+ def process(documents)
+ type = self.class.reference_type
+
+ nodes = documents.flat_map do |document|
+ Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
+ end
+
+ gather_references(nodes)
+ end
+
+ # Gathers the references for the given HTML nodes.
+ def gather_references(nodes)
+ nodes = nodes_user_can_reference(current_user, nodes)
+ nodes = nodes_visible_to_user(current_user, nodes)
+
+ referenced_by(nodes)
+ end
+
+ # Returns a Hash containing the projects for a given list of HTML nodes.
+ #
+ # The returned Hash uses the following format:
+ #
+ # { project ID => project }
+ #
+ def projects_for_nodes(nodes)
+ @projects_for_nodes ||=
+ grouped_objects_for_nodes(nodes, Project, 'data-project')
+ end
+
+ def can?(user, permission, subject)
+ Ability.abilities.allowed?(user, permission, subject)
+ end
+
+ def find_projects_for_hash_keys(hash)
+ Project.where(id: hash.keys)
+ end
+
+ private
+
+ attr_reader :current_user, :project
+
+ def lazy(&block)
+ Gitlab::Lazy.new(&block)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb
new file mode 100644
index 00000000000..0fee9d267de
--- /dev/null
+++ b/lib/banzai/reference_parser/commit_parser.rb
@@ -0,0 +1,34 @@
+module Banzai
+ module ReferenceParser
+ class CommitParser < BaseParser
+ self.reference_type = :commit
+
+ def referenced_by(nodes)
+ commit_ids = commit_ids_per_project(nodes)
+ projects = find_projects_for_hash_keys(commit_ids)
+
+ projects.flat_map do |project|
+ find_commits(project, commit_ids[project.id])
+ end
+ end
+
+ def commit_ids_per_project(nodes)
+ gather_attributes_per_project(nodes, self.class.data_attribute)
+ end
+
+ def find_commits(project, ids)
+ commits = []
+
+ return commits unless project.valid_repo?
+
+ ids.each do |id|
+ commit = project.commit(id)
+
+ commits << commit if commit
+ end
+
+ commits
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
new file mode 100644
index 00000000000..69d01f8db15
--- /dev/null
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -0,0 +1,38 @@
+module Banzai
+ module ReferenceParser
+ class CommitRangeParser < BaseParser
+ self.reference_type = :commit_range
+
+ def referenced_by(nodes)
+ range_ids = commit_range_ids_per_project(nodes)
+ projects = find_projects_for_hash_keys(range_ids)
+
+ projects.flat_map do |project|
+ find_ranges(project, range_ids[project.id])
+ end
+ end
+
+ def commit_range_ids_per_project(nodes)
+ gather_attributes_per_project(nodes, self.class.data_attribute)
+ end
+
+ def find_ranges(project, range_ids)
+ ranges = []
+
+ range_ids.each do |id|
+ range = find_object(project, id)
+
+ ranges << range if range
+ end
+
+ ranges
+ end
+
+ def find_object(project, id)
+ range = CommitRange.new(id, project)
+
+ range.valid_commits? ? range : nil
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
new file mode 100644
index 00000000000..a1264db2111
--- /dev/null
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -0,0 +1,25 @@
+module Banzai
+ module ReferenceParser
+ class ExternalIssueParser < BaseParser
+ self.reference_type = :external_issue
+
+ def referenced_by(nodes)
+ issue_ids = issue_ids_per_project(nodes)
+ projects = find_projects_for_hash_keys(issue_ids)
+ issues = []
+
+ projects.each do |project|
+ issue_ids[project.id].each do |id|
+ issues << ExternalIssue.new(id, project)
+ end
+ end
+
+ issues
+ end
+
+ def issue_ids_per_project(nodes)
+ gather_attributes_per_project(nodes, self.class.data_attribute)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
new file mode 100644
index 00000000000..f306079d833
--- /dev/null
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -0,0 +1,54 @@
+module Banzai
+ module ReferenceParser
+ class IssueParser < BaseParser
+ self.reference_type = :issue
+
+ def nodes_visible_to_user(user, nodes)
+ # It is not possible to check access rights for external issue trackers
+ return nodes if project && project.external_issue_tracker
+
+ issues = issues_for_nodes(nodes)
+
+ nodes.select do |node|
+ issue = issue_for_node(issues, node)
+
+ issue ? can?(user, :read_issue, issue) : false
+ end
+ end
+
+ def referenced_by(nodes)
+ issues = issues_for_nodes(nodes)
+
+ nodes.map { |node| issue_for_node(issues, node) }.uniq
+ end
+
+ def issues_for_nodes(nodes)
+ @issues_for_nodes ||= grouped_objects_for_nodes(
+ nodes,
+ Issue.all.includes(
+ :author,
+ :assignee,
+ {
+ # These associations are primarily used for checking permissions.
+ # Eager loading these ensures we don't end up running dozens of
+ # queries in this process.
+ project: [
+ { namespace: :owner },
+ { group: [:owners, :group_members] },
+ :invited_groups,
+ :project_members
+ ]
+ }
+ ),
+ self.class.data_attribute
+ )
+ end
+
+ private
+
+ def issue_for_node(issues, node)
+ issues[node.attr(self.class.data_attribute).to_i]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb
new file mode 100644
index 00000000000..e5d1eb11d7f
--- /dev/null
+++ b/lib/banzai/reference_parser/label_parser.rb
@@ -0,0 +1,11 @@
+module Banzai
+ module ReferenceParser
+ class LabelParser < BaseParser
+ self.reference_type = :label
+
+ def references_relation
+ Label
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
new file mode 100644
index 00000000000..c9a9ca79c09
--- /dev/null
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -0,0 +1,11 @@
+module Banzai
+ module ReferenceParser
+ class MergeRequestParser < BaseParser
+ self.reference_type = :merge_request
+
+ def references_relation
+ MergeRequest.includes(:author, :assignee, :target_project)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb
new file mode 100644
index 00000000000..a000ac61e5c
--- /dev/null
+++ b/lib/banzai/reference_parser/milestone_parser.rb
@@ -0,0 +1,11 @@
+module Banzai
+ module ReferenceParser
+ class MilestoneParser < BaseParser
+ self.reference_type = :milestone
+
+ def references_relation
+ Milestone
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
new file mode 100644
index 00000000000..fa71b3c952a
--- /dev/null
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -0,0 +1,11 @@
+module Banzai
+ module ReferenceParser
+ class SnippetParser < BaseParser
+ self.reference_type = :snippet
+
+ def references_relation
+ Snippet
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
new file mode 100644
index 00000000000..a12b0d19560
--- /dev/null
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -0,0 +1,92 @@
+module Banzai
+ module ReferenceParser
+ class UserParser < BaseParser
+ self.reference_type = :user
+
+ def referenced_by(nodes)
+ group_ids = []
+ user_ids = []
+ project_ids = []
+
+ nodes.each do |node|
+ if node.has_attribute?('data-group')
+ group_ids << node.attr('data-group').to_i
+ elsif node.has_attribute?(self.class.data_attribute)
+ user_ids << node.attr(self.class.data_attribute).to_i
+ elsif node.has_attribute?('data-project')
+ project_ids << node.attr('data-project').to_i
+ end
+ end
+
+ find_users_for_groups(group_ids) | find_users(user_ids) |
+ find_users_for_projects(project_ids)
+ end
+
+ def nodes_visible_to_user(user, nodes)
+ group_attr = 'data-group'
+ groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) }
+ visible = []
+ remaining = []
+
+ nodes.each do |node|
+ if node.has_attribute?(group_attr)
+ node_group = groups[node.attr(group_attr).to_i]
+
+ if node_group &&
+ can?(user, :read_group, node_group)
+ visible << node
+ end
+ # Remaining nodes will be processed by the parent class'
+ # implementation of this method.
+ else
+ remaining << node
+ end
+ end
+
+ visible + super(current_user, remaining)
+ end
+
+ def nodes_user_can_reference(current_user, nodes)
+ project_attr = 'data-project'
+ author_attr = 'data-author'
+
+ projects = lazy { projects_for_nodes(nodes) }
+ users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) }
+
+ nodes.select do |node|
+ project_id = node.attr(project_attr)
+ user_id = node.attr(author_attr)
+
+ if project && project_id && project.id == project_id.to_i
+ true
+ elsif project_id && user_id
+ project = projects[project_id.to_i]
+ user = users[user_id.to_i]
+
+ project && user ? project.team.member?(user) : false
+ else
+ true
+ end
+ end
+ end
+
+ def find_users(ids)
+ return [] if ids.empty?
+
+ User.where(id: ids).to_a
+ end
+
+ def find_users_for_groups(ids)
+ return [] if ids.empty?
+
+ User.joins(:group_members).where(members: { source_id: ids }).to_a
+ end
+
+ def find_users_for_projects(ids)
+ return [] if ids.empty?
+
+ Project.where(id: ids).flat_map { |p| p.team.members.to_a }
+ end
+ end
+ end
+end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index c14a9c4c722..6718acdef7e 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -30,13 +30,9 @@ module Banzai
end
def self.render_result(text, context = {})
- Pipeline[context[:pipeline]].call(text, context)
- end
+ text = Pipeline[:pre_process].to_html(text, context) if text
- def self.pre_process(text, context)
- pipeline = Pipeline[:pre_process]
-
- pipeline.to_html(text, context)
+ Pipeline[context[:pipeline]].call(text, context)
end
# Perform post-processing on an HTML String
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index ac6d667cf8d..229050151d3 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -23,8 +23,8 @@ module Ci
cross: 0x10,
}
- def self.convert(ansi)
- Converter.new().convert(ansi)
+ def self.convert(ansi, state = nil)
+ Converter.new.convert(ansi, state)
end
class Converter
@@ -84,22 +84,38 @@ module Ci
def on_107(s) set_bg_color(7, 'l') end
def on_109(s) set_bg_color(9, 'l') end
- def convert(ansi)
- @out = ""
- @n_open_tags = 0
- reset()
+ attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
+
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
+
+ def convert(raw, new_state)
+ reset_state
+ restore_state(raw, new_state) if new_state.present?
+
+ start = @offset
+ ansi = raw[@offset..-1]
+
+ open_new_tag
- s = StringScanner.new(ansi.gsub("<", "&lt;"))
- while(!s.eos?)
+ s = StringScanner.new(ansi)
+ until s.eos?
if s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
+ elsif s.scan(/\e(([@-_])(.*?)?)?$/)
+ break
+ elsif s.scan(/</)
+ @out << '&lt;'
+ elsif s.scan(/\n/)
+ @out << '<br>'
else
@out << s.scan(/./m)
end
+ @offset += s.matched_size
end
close_open_tags()
- @out
+
+ { state: state, html: @out, text: ansi[0, @offset - start], append: start > 0 }
end
def handle_sequence(s)
@@ -121,6 +137,20 @@ module Ci
evaluate_command_stack(commands)
+ open_new_tag
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.send("on_#{command}", stack)
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag
css_classes = []
unless @fg_color.nil?
@@ -138,20 +168,8 @@ module Ci
css_classes << "term-#{css_class}" if @style_mask & flag != 0
end
- open_new_tag(css_classes) if css_classes.length > 0
- end
+ return if css_classes.empty?
- def evaluate_command_stack(stack)
- return unless command = stack.shift()
-
- if self.respond_to?("on_#{command}", true)
- self.send("on_#{command}", stack)
- end
-
- evaluate_command_stack(stack)
- end
-
- def open_new_tag(css_classes)
@out << %{<span class="#{css_classes.join(' ')}">}
@n_open_tags += 1
end
@@ -163,6 +181,31 @@ module Ci
end
end
+ def reset_state
+ @offset = 0
+ @n_open_tags = 0
+ @out = ''
+ reset
+ end
+
+ def state
+ state = STATE_PARAMS.inject({}) do |h, param|
+ h[param] = send(param)
+ h
+ end
+ Base64.urlsafe_encode64(state.to_json)
+ end
+
+ def restore_state(raw, new_state)
+ state = Base64.urlsafe_decode64(new_state)
+ state = JSON.parse(state, symbolize_names: true)
+ return if state[:offset].to_i > raw.length
+
+ STATE_PARAMS.each do |param|
+ send("#{param}=".to_sym, state[param])
+ end
+ end
+
def reset
@fg_color = nil
@bg_color = nil
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 607359769d1..260ac81f5fa 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -114,6 +114,7 @@ module Ci
# id (required) - The ID of a build
# token (required) - The build authorization token
# file (required) - Artifacts file
+ # expire_in (optional) - Specify when artifacts should expire (ex. 7d)
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
@@ -145,6 +146,7 @@ module Ci
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
+ build.artifacts_expire_in = params['expire_in']
if build.save
present(build, with: Entities::BuildDetails)
@@ -193,8 +195,7 @@ module Ci
not_found! unless build
authenticate_build_token!(build)
- build.remove_artifacts_file!
- build.remove_artifacts_metadata!
+ build.erase_artifacts!
end
end
end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index b25e0e573a8..3f5bdaba3f5 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -20,7 +20,7 @@ module Ci
expose :name, :token, :stage
expose :project_id
expose :project_name
- expose :artifacts_file, using: ArtifactFile, if: lambda { |build, opts| build.artifacts? }
+ expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
end
class BuildDetails < Build
@@ -29,6 +29,7 @@ module Ci
expose :before_sha
expose :allow_git_fetch
expose :token
+ expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? }
expose :options do |model|
model.options
@@ -56,7 +57,7 @@ module Ci
class TriggerRequest < Grape::Entity
expose :id, :variables
- expose :commit, using: Commit
+ expose :pipeline, using: Commit, as: :commit
end
end
end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index 192b1d18a51..bcc82969eb3 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -28,20 +28,17 @@ module Ci
post "register" do
required_attributes! [:token]
+ attributes = attributes_for_keys(
+ [:description, :tag_list, :run_untagged, :locked]
+ )
+
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(
- description: params[:description],
- tag_list: params[:tag_list],
- is_shared: true
- )
+ Ci::Runner.create(attributes.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
- project.runners.create(
- description: params[:description],
- tag_list: params[:tag_list]
- )
+ project.runners.create(attributes)
end
return forbidden! unless runner
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
index d53bdcbd0f2..1d7126a432d 100644
--- a/lib/ci/charts.rb
+++ b/lib/ci/charts.rb
@@ -13,7 +13,6 @@ module Ci
collect
end
-
def push(from, to, format)
@labels << from.strftime(format)
@total << project.builds.
@@ -60,11 +59,12 @@ module Ci
class BuildTime < Chart
def collect
- commits = project.ci_commits.last(30)
+ commits = project.pipelines.last(30)
commits.each do |commit|
@labels << commit.short_sha
- @build_times << (commit.duration / 60)
+ duration = commit.duration || 0
+ @build_times << (duration / 60)
end
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 504d3df9d34..01ef13df57a 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -1,33 +1,41 @@
module Ci
class GitlabCiYamlProcessor
- class ValidationError < StandardError;end
+ class ValidationError < StandardError; end
+
+ include Gitlab::Ci::Config::Node::LegacyValidationHelpers
- DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
- :dependencies, :before_script, :after_script, :variables]
+ :dependencies, :before_script, :after_script, :variables,
+ :environment]
+ ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
+ ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
- attr_reader :before_script, :after_script, :image, :services, :path, :cache
+ attr_reader :path, :cache, :stages
def initialize(config, path = nil)
- @config = YAML.safe_load(config, [Symbol], [], true)
+ @ci_config = Gitlab::Ci::Config.new(config)
+ @config = @ci_config.to_hash
+
@path = path
- unless @config.is_a? Hash
- raise ValidationError, "YAML should be a hash"
+ unless @ci_config.valid?
+ raise ValidationError, @ci_config.errors.first
end
- @config = @config.deep_symbolize_keys
-
initial_parsing
-
validate!
+ rescue Gitlab::Ci::Config::Loader::FormatError => e
+ raise ValidationError, e.message
end
def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
- builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
+ builds.select do |build|
+ build[:stage] == stage &&
+ process?(build[:only], build[:except], ref, tag, trigger_request)
+ end
end
def builds
@@ -36,10 +44,6 @@ module Ci
end
end
- def stages
- @stages || DEFAULT_STAGES
- end
-
def global_variables
@variables
end
@@ -48,19 +52,20 @@ module Ci
job = @jobs[name.to_sym]
return [] unless job
- job.fetch(:variables, [])
+ job[:variables] || []
end
private
def initial_parsing
- @before_script = @config[:before_script] || []
- @after_script = @config[:after_script]
- @image = @config[:image]
- @services = @config[:services]
- @stages = @config[:stages] || @config[:types]
- @variables = @config[:variables] || {}
- @cache = @config[:cache]
+ @before_script = @ci_config.before_script
+ @image = @ci_config.image
+ @after_script = @ci_config.after_script
+ @services = @ci_config.services
+ @variables = @ci_config.variables
+ @stages = @ci_config.stages
+ @cache = @ci_config.cache
+
@jobs = {}
@config.except!(*ALLOWED_YAML_KEYS)
@@ -80,15 +85,21 @@ module Ci
def build_job(name, job)
{
- stage_idx: stages.index(job[:stage]),
+ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
- commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"),
+ ##
+ # Refactoring note:
+ # - before script behaves differently than after script
+ # - after script returns an array of commands
+ # - before script should be a concatenated command
+ commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
tag_list: job[:tags] || [],
name: name,
only: job[:only],
except: job[:except],
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
+ environment: job[:environment],
options: {
image: job[:image] || @image,
services: job[:services] || @services,
@@ -101,8 +112,6 @@ module Ci
end
def validate!
- validate_global!
-
@jobs.each do |name, job|
validate_job!(name, job)
end
@@ -110,48 +119,6 @@ module Ci
true
end
- def validate_global!
- unless validate_array_of_strings(@before_script)
- raise ValidationError, "before_script should be an array of strings"
- end
-
- unless @after_script.nil? || validate_array_of_strings(@after_script)
- raise ValidationError, "after_script should be an array of strings"
- end
-
- unless @image.nil? || @image.is_a?(String)
- raise ValidationError, "image should be a string"
- end
-
- unless @services.nil? || validate_array_of_strings(@services)
- raise ValidationError, "services should be an array of strings"
- end
-
- unless @stages.nil? || validate_array_of_strings(@stages)
- raise ValidationError, "stages should be an array of strings"
- end
-
- unless @variables.nil? || validate_variables(@variables)
- raise ValidationError, "variables should be a map of key-value strings"
- end
-
- validate_global_cache! if @cache
- end
-
- def validate_global_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
-
- if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
- raise ValidationError, "cache:paths parameter should be an array of strings"
- end
- end
-
def validate_job!(name, job)
validate_job_name!(name)
validate_job_keys!(name, job)
@@ -192,21 +159,25 @@ module Ci
raise ValidationError, "#{name} job: tags parameter should be an array of strings"
end
- if job[:only] && !validate_array_of_strings(job[:only])
- raise ValidationError, "#{name} job: only parameter should be an array of strings"
+ if job[:only] && !validate_array_of_strings_or_regexps(job[:only])
+ raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps"
end
- if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name} job: except parameter should be an array of strings"
+ if job[:except] && !validate_array_of_strings_or_regexps(job[:except])
+ raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps"
end
if job[:allow_failure] && !validate_boolean(job[:allow_failure])
raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end
- if job[:when] && !job[:when].in?(%w(on_success on_failure always))
+ if job[:when] && !job[:when].in?(%w[on_success on_failure always])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
+
+ if job[:environment] && !validate_environment(job[:environment])
+ raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}"
+ end
end
def validate_job_script!(name, job)
@@ -224,8 +195,8 @@ module Ci
end
def validate_job_stage!(name, job)
- unless job[:stage].is_a?(String) && job[:stage].in?(stages)
- raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
+ unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
+ raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
end
end
@@ -237,6 +208,12 @@ module Ci
end
def validate_job_cache!(name, job)
+ job[:cache].keys.each do |key|
+ unless ALLOWED_CACHE_KEYS.include? key
+ raise ValidationError, "#{name} job: cache unknown parameter #{key}"
+ end
+ end
+
if job[:cache][:key] && !validate_string(job[:cache][:key])
raise ValidationError, "#{name} job: cache:key parameter should be a string"
end
@@ -251,6 +228,12 @@ module Ci
end
def validate_job_artifacts!(name, job)
+ job[:artifacts].keys.each do |key|
+ unless ALLOWED_ARTIFACTS_KEYS.include? key
+ raise ValidationError, "#{name} job: artifacts unknown parameter #{key}"
+ end
+ end
+
if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
end
@@ -262,40 +245,32 @@ module Ci
if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths])
raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings"
end
+
+ if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always])
+ raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always"
+ end
+
+ if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in])
+ raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration"
+ end
end
def validate_job_dependencies!(name, job)
- if !validate_array_of_strings(job[:dependencies])
+ unless 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])
+ stage_index = @stages.index(job[:stage])
job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
- unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
+ unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
- def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? { |value| validate_string(value) }
- end
-
- def validate_variables(variables)
- variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
- end
-
- def validate_string(value)
- value.is_a?(String) || value.is_a?(Symbol)
- end
-
- def validate_boolean(value)
- value.in?([true, false])
- end
-
def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request)
diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb
new file mode 100644
index 00000000000..eb5a2596177
--- /dev/null
+++ b/lib/container_registry/blob.rb
@@ -0,0 +1,48 @@
+module ContainerRegistry
+ class Blob
+ attr_reader :repository, :config
+
+ delegate :registry, :client, to: :repository
+
+ def initialize(repository, config)
+ @repository = repository
+ @config = config || {}
+ end
+
+ def valid?
+ digest.present?
+ end
+
+ def path
+ "#{repository.path}@#{digest}"
+ end
+
+ def digest
+ config['digest'] || config['blobSum']
+ end
+
+ def type
+ config['mediaType']
+ end
+
+ def size
+ config['size']
+ end
+
+ def revision
+ digest.split(':')[1]
+ end
+
+ def short_revision
+ revision[0..8]
+ end
+
+ def delete
+ client.delete_blob(repository.name, digest)
+ end
+
+ def data
+ @data ||= client.blob(repository.name, digest, type)
+ end
+ end
+end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
new file mode 100644
index 00000000000..42232b7129d
--- /dev/null
+++ b/lib/container_registry/client.rb
@@ -0,0 +1,68 @@
+require 'faraday'
+require 'faraday_middleware'
+
+module ContainerRegistry
+ class Client
+ attr_accessor :uri
+
+ MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+
+ def initialize(base_uri, options = {})
+ @base_uri = base_uri
+ @faraday = Faraday.new(@base_uri) do |conn|
+ initialize_connection(conn, options)
+ end
+ end
+
+ def repository_tags(name)
+ response_body @faraday.get("/v2/#{name}/tags/list")
+ end
+
+ def repository_manifest(name, reference)
+ response_body @faraday.get("/v2/#{name}/manifests/#{reference}")
+ end
+
+ def repository_tag_digest(name, reference)
+ response = @faraday.head("/v2/#{name}/manifests/#{reference}")
+ response.headers['docker-content-digest'] if response.success?
+ end
+
+ def delete_repository_tag(name, reference)
+ @faraday.delete("/v2/#{name}/manifests/#{reference}").success?
+ end
+
+ def blob(name, digest, type = nil)
+ headers = {}
+ headers['Accept'] = type if type
+ response_body @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers)
+ end
+
+ def delete_blob(name, digest)
+ @faraday.delete("/v2/#{name}/blobs/#{digest}").success?
+ end
+
+ private
+
+ def initialize_connection(conn, options)
+ conn.request :json
+ conn.headers['Accept'] = MANIFEST_VERSION
+
+ conn.response :json, content_type: 'application/json'
+ conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws'
+ conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json'
+ conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v2+json'
+
+ if options[:user] && options[:password]
+ conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
+ elsif options[:token]
+ conn.request(:authorization, :bearer, options[:token].to_s)
+ end
+
+ conn.adapter :net_http
+ end
+
+ def response_body(response)
+ response.body if response.success?
+ end
+ end
+end
diff --git a/lib/container_registry/config.rb b/lib/container_registry/config.rb
new file mode 100644
index 00000000000..589f9f4380a
--- /dev/null
+++ b/lib/container_registry/config.rb
@@ -0,0 +1,16 @@
+module ContainerRegistry
+ class Config
+ attr_reader :tag, :blob, :data
+
+ def initialize(tag, blob)
+ @tag, @blob = tag, blob
+ @data = JSON.parse(blob.data)
+ end
+
+ def [](key)
+ return unless data
+
+ data[key]
+ end
+ end
+end
diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb
new file mode 100644
index 00000000000..0e634f6b6ef
--- /dev/null
+++ b/lib/container_registry/registry.rb
@@ -0,0 +1,21 @@
+module ContainerRegistry
+ class Registry
+ attr_reader :uri, :client, :path
+
+ def initialize(uri, options = {})
+ @uri = uri
+ @path = options[:path] || default_path
+ @client = ContainerRegistry::Client.new(uri, options)
+ end
+
+ def repository(name)
+ ContainerRegistry::Repository.new(self, name)
+ end
+
+ private
+
+ def default_path
+ @uri.sub(/^https?:\/\//, '')
+ end
+ end
+end
diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb
new file mode 100644
index 00000000000..0e4a7cb3cc9
--- /dev/null
+++ b/lib/container_registry/repository.rb
@@ -0,0 +1,48 @@
+module ContainerRegistry
+ class Repository
+ attr_reader :registry, :name
+
+ delegate :client, to: :registry
+
+ def initialize(registry, name)
+ @registry, @name = registry, name
+ end
+
+ def path
+ [registry.path, name].compact.join('/')
+ end
+
+ def tag(tag)
+ ContainerRegistry::Tag.new(self, tag)
+ end
+
+ def manifest
+ return @manifest if defined?(@manifest)
+
+ @manifest = client.repository_tags(name)
+ end
+
+ def valid?
+ manifest.present?
+ end
+
+ def tags
+ return @tags if defined?(@tags)
+ return [] unless manifest && manifest['tags']
+
+ @tags = manifest['tags'].map do |tag|
+ ContainerRegistry::Tag.new(self, tag)
+ end
+ end
+
+ def blob(config)
+ ContainerRegistry::Blob.new(self, config)
+ end
+
+ def delete_tags
+ return unless tags
+
+ tags.all?(&:delete)
+ end
+ end
+end
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
new file mode 100644
index 00000000000..708d01b95a1
--- /dev/null
+++ b/lib/container_registry/tag.rb
@@ -0,0 +1,88 @@
+module ContainerRegistry
+ class Tag
+ attr_reader :repository, :name
+
+ delegate :registry, :client, to: :repository
+ delegate :revision, :short_revision, to: :config_blob, allow_nil: true
+
+ def initialize(repository, name)
+ @repository, @name = repository, name
+ end
+
+ def valid?
+ manifest.present?
+ end
+
+ def v1?
+ manifest && manifest['schemaVersion'] == 1
+ end
+
+ def v2?
+ manifest && manifest['schemaVersion'] == 2
+ end
+
+ def manifest
+ return @manifest if defined?(@manifest)
+
+ @manifest = client.repository_manifest(repository.name, name)
+ end
+
+ def path
+ "#{repository.path}:#{name}"
+ end
+
+ def [](key)
+ return unless manifest
+
+ manifest[key]
+ end
+
+ def digest
+ return @digest if defined?(@digest)
+
+ @digest = client.repository_tag_digest(repository.name, name)
+ end
+
+ def config_blob
+ return @config_blob if defined?(@config_blob)
+ return unless manifest && manifest['config']
+
+ @config_blob = repository.blob(manifest['config'])
+ end
+
+ def config
+ return unless config_blob
+
+ @config ||= ContainerRegistry::Config.new(self, config_blob)
+ end
+
+ def created_at
+ return unless config
+
+ @created_at ||= DateTime.rfc3339(config['created'])
+ end
+
+ def layers
+ return @layers if defined?(@layers)
+ return unless manifest
+
+ layers = manifest['layers'] || manifest['fsLayers']
+
+ @layers = layers.map do |layer|
+ repository.blob(layer)
+ end
+ end
+
+ def total_size
+ return unless layers
+
+ layers.map(&:size).sum if v2?
+ end
+
+ def delete
+ return unless digest
+
+ client.delete_repository_tag(repository.name, digest)
+ end
+ end
+end
diff --git a/lib/disable_email_interceptor.rb b/lib/disable_email_interceptor.rb
index 1b80be112a4..cee664b8951 100644
--- a/lib/disable_email_interceptor.rb
+++ b/lib/disable_email_interceptor.rb
@@ -1,6 +1,5 @@
# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails
class DisableEmailInterceptor
-
def self.delivering_email(message)
message.perform_deliveries = false
Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}"
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index f15b2cfd231..668d2fa41b3 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -27,7 +27,7 @@ class EventFilter
@params = if params
params.dup
else
- []#EventFilter.default_filter
+ [] # EventFilter.default_filter
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 37f4c34054f..c3064163e07 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -2,6 +2,11 @@ require_dependency 'gitlab/git'
module Gitlab
def self.com?
- Gitlab.config.gitlab.url == 'https://gitlab.com'
+ # Check `staging?` as well to keep parity with gitlab.com
+ Gitlab.config.gitlab.url == 'https://gitlab.com' || staging?
+ end
+
+ def self.staging?
+ Gitlab.config.gitlab.url == 'https://staging.gitlab.com'
end
end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 6d0e30e916f..831f1e635ba 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,6 +5,8 @@
#
module Gitlab
module Access
+ class AccessDeniedError < StandardError; end
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 0b9c2e730f9..1a22ad9acf5 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -4,7 +4,6 @@ module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
# the resulting HTML through HTML pipeline filters.
module Asciidoc
-
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
'env-gitlab', 'source-highlighter=html-pipeline'
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 30509528b8b..db1704af75e 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,17 +1,86 @@
module Gitlab
- class Auth
- def find(login, password)
- user = User.by_login(login)
-
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- return nil unless Gitlab::LDAP::Config.enabled?
-
- Gitlab::LDAP::Authentication.login(login, password)
- else
- user if user.valid_password?(password)
+ module Auth
+ Result = Struct.new(:user, :type)
+
+ class << self
+ def find_for_git_client(login, password, project:, ip:)
+ raise "Must provide an IP for rate limiting" if ip.nil?
+
+ result = Result.new
+
+ if valid_ci_request?(login, password, project)
+ result.type = :ci
+ elsif result.user = find_with_user_password(login, password)
+ result.type = :gitlab_or_ldap
+ elsif result.user = oauth_access_token_check(login, password)
+ result.type = :oauth
+ end
+
+ rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
+ result
+ end
+
+ def find_with_user_password(login, password)
+ user = User.by_login(login)
+
+ # If no user is found, or it's an LDAP server, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ if user.nil? || user.ldap_user?
+ # Second chance - try LDAP authentication
+ return nil unless Gitlab::LDAP::Config.enabled?
+
+ Gitlab::LDAP::Authentication.login(login, password)
+ else
+ user if user.valid_password?(password)
+ end
+ end
+
+ def rate_limit!(ip, success:, login:)
+ rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip)
+ return unless rate_limiter.enabled?
+
+ if success
+ # Repeated login 'failures' are normal behavior for some Git clients so
+ # it is important to reset the ban counter once the client has proven
+ # they are not a 'bad guy'.
+ rate_limiter.reset!
+ else
+ # Register a login failure so that Rack::Attack can block the next
+ # request from this IP if needed.
+ rate_limiter.register_fail!
+
+ if rate_limiter.banned?
+ Rails.logger.info "IP #{ip} failed to login " \
+ "as #{login} but has been temporarily banned from Git auth"
+ end
+ end
+ end
+
+ private
+
+ def valid_ci_request?(login, password, project)
+ matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
+
+ return false unless project && matched_login.present?
+
+ underscored_service = matched_login['service'].underscore
+
+ if underscored_service == 'gitlab_ci'
+ project && project.valid_build_token?(password)
+ elsif Service.available_services_names.include?(underscored_service)
+ # We treat underscored_service as a trusted input because it is included
+ # in the Service.available_services_names whitelist.
+ service = project.public_send("#{underscored_service}_service")
+
+ service && service.activated? && service.valid_token?(password)
+ end
+ end
+
+ def oauth_access_token_check(login, password)
+ if login == "oauth2" && password.present?
+ token = Doorkeeper::AccessToken.by_token(password)
+ token && token.accessible? && User.find_by(id: token.resource_owner_id)
+ end
end
end
end
diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb
new file mode 100644
index 00000000000..1089bc9f89e
--- /dev/null
+++ b/lib/gitlab/auth/ip_rate_limiter.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Auth
+ class IpRateLimiter
+ attr_reader :ip
+
+ def initialize(ip)
+ @ip = ip
+ @banned = false
+ end
+
+ def enabled?
+ config.enabled
+ end
+
+ def reset!
+ Rack::Attack::Allow2Ban.reset(ip, config)
+ end
+
+ def register_fail!
+ # Allow2Ban.filter will return false if this IP has not failed too often yet
+ @banned = Rack::Attack::Allow2Ban.filter(ip, config) do
+ # If we return false here, the failure for this IP is ignored by Allow2Ban
+ ip_can_be_banned?
+ end
+ end
+
+ def banned?
+ @banned
+ end
+
+ private
+
+ def config
+ Gitlab.config.rack_attack.git_basic_auth
+ end
+
+ def ip_can_be_banned?
+ config.ip_whitelist.exclude?(ip)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
new file mode 100644
index 00000000000..c94bfc0e65f
--- /dev/null
+++ b/lib/gitlab/award_emoji.rb
@@ -0,0 +1,93 @@
+module Gitlab
+ class AwardEmoji
+ CATEGORIES = {
+ other: "Other",
+ objects: "Objects",
+ places: "Places",
+ travel_places: "Travel",
+ emoticons: "Emoticons",
+ objects_symbols: "Symbols",
+ nature: "Nature",
+ celebration: "Celebration",
+ people: "People",
+ activity: "Activity",
+ flags: "Flags",
+ food_drink: "Food"
+ }.with_indifferent_access
+
+ CATEGORY_ALIASES = {
+ symbols: "objects_symbols",
+ foods: "food_drink",
+ travel: "travel_places"
+ }.with_indifferent_access
+
+ def self.normalize_emoji_name(name)
+ aliases[name] || name
+ end
+
+ def self.emoji_by_category
+ unless @emoji_by_category
+ @emoji_by_category = Hash.new { |h, key| h[key] = [] }
+
+ emojis.each do |emoji_name, data|
+ data["name"] = emoji_name
+
+ # Skip Fitzpatrick(tone) modifiers
+ next if data["category"] == "modifier"
+
+ category = CATEGORY_ALIASES[data["category"]] || data["category"]
+
+ @emoji_by_category[category] << data
+ end
+
+ @emoji_by_category = @emoji_by_category.sort.to_h
+ end
+
+ @emoji_by_category
+ end
+
+ def self.emojis
+ @emojis ||=
+ begin
+ json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
+ JSON.parse(File.read(json_path))
+ end
+ end
+
+ def self.aliases
+ @aliases ||=
+ begin
+ json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
+ JSON.parse(File.read(json_path))
+ end
+ end
+
+ # Returns an Array of Emoji names and their asset URLs.
+ def self.urls
+ @urls ||= begin
+ path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
+ # Construct the full asset path ourselves because
+ # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
+ # of entries since it has to do a lot of extra work (e.g. regexps).
+ prefix = Gitlab::Application.config.assets.prefix
+ digest = Gitlab::Application.config.assets.digest
+ base =
+ if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
+ Gitlab::Application.config.relative_url_root
+ else
+ ''
+ end
+
+ JSON.parse(File.read(path)).map do |hash|
+ if digest
+ fname = "#{hash['unicode']}-#{hash['digest']}"
+ else
+ fname = hash['unicode']
+ end
+
+ { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index cdcaae8094c..478f145bfed 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -1,5 +1,3 @@
-require_relative 'shell_env'
-
module Grack
class AuthSpawner
def self.call(env)
@@ -10,7 +8,6 @@ module Grack
end
class Auth < Rack::Auth::Basic
-
attr_accessor :user, :project, :env
def call(env)
@@ -24,7 +21,7 @@ module Grack
# Need this if under RELATIVE_URL_ROOT
unless Gitlab.config.gitlab.relative_url_root.empty?
# If website is mounted using relative_url_root need to remove it first
- @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'')
+ @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root, '')
else
@env['PATH_INFO'] = @request.path
end
@@ -33,13 +30,10 @@ module Grack
auth!
- lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
return lfs_response unless lfs_response.nil?
- if project && authorized_request?
- # Tell gitlab-workhorse the request is OK, and what the GL_ID is
- render_grack_auth_ok
- elsif @user.nil? && !@ci
+ if @user.nil? && !@ci
unauthorized
else
render_not_found
@@ -64,11 +58,6 @@ module Grack
end
@user = authenticate_user(login, password)
-
- if @user
- Gitlab::ShellEnv.set_env(@user)
- @env['REMOTE_USER'] = @auth.username
- end
end
def ci_request?(login, password)
@@ -98,7 +87,7 @@ module Grack
end
def authenticate_user(login, password)
- user = Gitlab::Auth.new.find(login, password)
+ user = Gitlab::Auth.find_with_user_password(login, password)
unless user
user = oauth_access_token_check(login, password)
@@ -141,36 +130,6 @@ module Grack
user
end
- def authorized_request?
- return true if @ci
-
- case git_cmd
- when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
- if !Gitlab.config.gitlab_shell.upload_pack
- false
- elsif user
- Gitlab::GitAccess.new(user, project).download_access_check.allowed?
- elsif project.public?
- # Allow clone/fetch for public projects
- true
- else
- false
- end
- when *Gitlab::GitAccess::PUSH_COMMANDS
- if !Gitlab.config.gitlab_shell.receive_pack
- false
- elsif user
- # Skip user authorization on upload request.
- # It will be done by the pre-receive hook in the repository.
- true
- else
- false
- end
- else
- false
- end
- end
-
def git_cmd
if @request.get?
@request.params['service']
@@ -197,24 +156,6 @@ module Grack
end
end
- def render_grack_auth_ok
- repo_path =
- if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
- ProjectWiki.new(project).repository.path_to_repo
- else
- project.repository.path_to_repo
- end
-
- [
- 200,
- { "Content-Type" => "application/json" },
- [JSON.dump({
- 'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
- 'RepoPath' => repo_path,
- })]
- ]
- end
-
def render_not_found
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 132f9cd1966..34e0143a82e 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -1,3 +1,5 @@
+require 'securerandom'
+
module Gitlab
class Shell
class Error < StandardError; end
@@ -18,77 +20,82 @@ module Gitlab
# Init new repository
#
+ # storage - project's storage path
# name - project path with namespace
#
# Ex.
- # add_repository("gitlab/gitlab-ci")
+ # add_repository("/path/to/storage", "gitlab/gitlab-ci")
#
- def add_repository(name)
+ def add_repository(storage, name)
Gitlab::Utils.system_silent([gitlab_shell_projects_path,
- 'add-project', "#{name}.git"])
+ 'add-project', storage, "#{name}.git"])
end
# Import repository
#
+ # storage - project's storage path
# name - project path with namespace
#
# Ex.
- # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git")
+ # import_repository("/path/to/storage", "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, '900'])
+ def import_repository(storage, name, url)
+ output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
+ storage, "#{name}.git", url, '900'])
raise Error, output unless status.zero?
true
end
# Move repository
- #
+ # storage - project's storage path
# path - project path with namespace
# new_path - new project path with namespace
#
# Ex.
- # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new")
+ # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
#
- def mv_repository(path, new_path)
+ def mv_repository(storage, path, new_path)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project',
- "#{path}.git", "#{new_path}.git"])
+ storage, "#{path}.git", "#{new_path}.git"])
end
# Fork repository to new namespace
- #
+ # storage - project's storage path
# path - project path with namespace
# fork_namespace - namespace for forked project
#
# Ex.
- # fork_repository("gitlab/gitlab-ci", "randx")
+ # fork_repository("/path/to/storage", "gitlab/gitlab-ci", "randx")
#
- def fork_repository(path, fork_namespace)
+ def fork_repository(storage, path, fork_namespace)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project',
- "#{path}.git", fork_namespace])
+ storage, "#{path}.git", fork_namespace])
end
# Remove repository from file system
#
+ # storage - project's storage path
# name - project path with namespace
#
# Ex.
- # remove_repository("gitlab/gitlab-ci")
+ # remove_repository("/path/to/storage", "gitlab/gitlab-ci")
#
- def remove_repository(name)
+ def remove_repository(storage, name)
Gitlab::Utils.system_silent([gitlab_shell_projects_path,
- 'rm-project', "#{name}.git"])
+ 'rm-project', storage, "#{name}.git"])
end
# Gc repository
#
+ # storage - project storage path
# path - project path with namespace
#
# Ex.
- # gc("gitlab/gitlab-ci")
+ # gc("/path/to/storage", "gitlab/gitlab-ci")
#
- def gc(path)
+ def gc(storage, path)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc',
- "#{path}.git"])
+ storage, "#{path}.git"])
end
# Add new key to gitlab-shell
@@ -133,31 +140,31 @@ module Gitlab
# Add empty directory for storing repositories
#
# Ex.
- # add_namespace("gitlab")
+ # add_namespace("/path/to/storage", "gitlab")
#
- def add_namespace(name)
- FileUtils.mkdir(full_path(name), mode: 0770) unless exists?(name)
+ def add_namespace(storage, name)
+ FileUtils.mkdir(full_path(storage, name), mode: 0770) unless exists?(storage, name)
end
# Remove directory from repositories storage
# Every repository inside this directory will be removed too
#
# Ex.
- # rm_namespace("gitlab")
+ # rm_namespace("/path/to/storage", "gitlab")
#
- def rm_namespace(name)
- FileUtils.rm_r(full_path(name), force: true)
+ def rm_namespace(storage, name)
+ FileUtils.rm_r(full_path(storage, name), force: true)
end
# Move namespace directory inside repositories storage
#
# Ex.
- # mv_namespace("gitlab", "gitlabhq")
+ # mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
- def mv_namespace(old_name, new_name)
- return false if exists?(new_name) || !exists?(old_name)
+ def mv_namespace(storage, old_name, new_name)
+ return false if exists?(storage, new_name) || !exists?(storage, old_name)
- FileUtils.mv(full_path(old_name), full_path(new_name))
+ FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
end
def url_to_repo(path)
@@ -176,11 +183,26 @@ module Gitlab
# Check if such directory exists in repositories.
#
# Usage:
- # exists?('gitlab')
- # exists?('gitlab/cookies.git')
+ # exists?(storage, 'gitlab')
+ # exists?(storage, 'gitlab/cookies.git')
#
- def exists?(dir_name)
- File.exists?(full_path(dir_name))
+ def exists?(storage, dir_name)
+ File.exist?(full_path(storage, dir_name))
+ end
+
+ # Create (if necessary) and link the secret token file
+ def generate_and_link_secret_token
+ secret_file = Gitlab.config.gitlab_shell.secret_file
+ unless File.exist? secret_file
+ # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
+ token = SecureRandom.hex(16)
+ File.write(secret_file, token)
+ end
+
+ link_path = File.join(gitlab_shell_path, '.gitlab_shell_secret')
+ if File.exist?(gitlab_shell_path) && !File.exist?(link_path)
+ FileUtils.symlink(secret_file, link_path)
+ end
end
protected
@@ -193,14 +215,10 @@ module Gitlab
File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}")
end
- def repos_path
- Gitlab.config.gitlab_shell.repos_path
- end
-
- def full_path(dir_name)
+ def full_path(storage, dir_name)
raise ArgumentError.new("Directory name can't be blank") if dir_name.blank?
- File.join(repos_path, dir_name)
+ File.join(storage, dir_name)
end
def gitlab_shell_projects_path
diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb
deleted file mode 100644
index 9f5adee594a..00000000000
--- a/lib/gitlab/backend/shell_env.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module Gitlab
- # This module provide 2 methods
- # to set specific ENV variables for GitLab Shell
- module ShellEnv
- extend self
-
- def set_env(user)
- # Set GL_ID env variable
- if user
- ENV['GL_ID'] = gl_id(user)
- end
- end
-
- def reset_env
- # Reset GL_ID env variable
- ENV['GL_ID'] = nil
- end
-
- def gl_id(user)
- if user.present?
- "user-#{user.id}"
- else
- # This empty string is used in the render_grack_auth_ok method
- ""
- end
- end
- end
-end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
index 9b83292ef33..8d1ad62fae0 100644
--- a/lib/gitlab/bitbucket_import/client.rb
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -121,7 +121,7 @@ module Gitlab
def get(url)
response = api.get(url)
- raise Unauthorized if (400..499).include?(response.code.to_i)
+ raise Unauthorized if (400..499).cover?(response.code.to_i)
response
end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index 941f818b847..b90ef0b0fba 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
name: repo["name"],
path: repo["slug"],
@@ -21,11 +21,8 @@ module Gitlab
import_type: "bitbucket",
import_source: "#{repo["owner"]}/#{repo["slug"]}",
import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
+ import_data: { credentials: { bb_session: session_data } }
).execute
-
- project.create_or_update_import_data(credentials: { bb_session: session_data })
-
- project
end
end
end
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
index 997a22779a0..d62bc50ce78 100644
--- a/lib/gitlab/blame.rb
+++ b/lib/gitlab/blame.rb
@@ -41,7 +41,8 @@ module Gitlab
def highlighted_lines
@blob.load_all_data!(repository)
- @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines
+ @highlighted_lines ||=
+ Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines
end
def project
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb
index 34e949130da..9f45aefda0f 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/build_data_builder.rb
@@ -3,7 +3,7 @@ module Gitlab
class << self
def build(build)
project = build.project
- commit = build.commit
+ commit = build.pipeline
user = build.user
data = {
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index f2020c82d40..cd2e83b4c27 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -56,7 +56,7 @@ module Gitlab
child_pattern = '[^/]*/?$' unless @opts[:recursive]
match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/
- until gz.eof? do
+ until gz.eof?
begin
path = read_string(gz).force_encoding('UTF-8')
meta = read_string(gz).force_encoding('UTF-8')
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
new file mode 100644
index 00000000000..e6cc1529760
--- /dev/null
+++ b/lib/gitlab/ci/config.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ ##
+ # Base GitLab CI Configuration facade
+ #
+ class Config
+ ##
+ # Temporary delegations that should be removed after refactoring
+ #
+ delegate :before_script, :image, :services, :after_script, :variables,
+ :stages, :cache, to: :@global
+
+ def initialize(config)
+ @config = Loader.new(config).load!
+
+ @global = Node::Global.new(@config)
+ @global.process!
+ end
+
+ def valid?
+ @global.valid?
+ end
+
+ def errors
+ @global.errors
+ end
+
+ def to_hash
+ @config
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
new file mode 100644
index 00000000000..dbf6eb0edbe
--- /dev/null
+++ b/lib/gitlab/ci/config/loader.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ class Config
+ class Loader
+ class FormatError < StandardError; end
+
+ def initialize(config)
+ @config = YAML.safe_load(config, [Symbol], [], true)
+ end
+
+ def valid?
+ @config.is_a?(Hash)
+ end
+
+ def load!
+ unless valid?
+ raise FormatError, 'Invalid configuration format'
+ end
+
+ @config.deep_symbolize_keys
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/boolean.rb b/lib/gitlab/ci/config/node/boolean.rb
new file mode 100644
index 00000000000..84b03ee7832
--- /dev/null
+++ b/lib/gitlab/ci/config/node/boolean.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a boolean value.
+ #
+ class Boolean < Entry
+ include Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/node/cache.rb
new file mode 100644
index 00000000000..cdf8ba2e35d
--- /dev/null
+++ b/lib/gitlab/ci/config/node/cache.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a cache configuration
+ #
+ class Cache < Entry
+ include Configurable
+
+ node :key, Node::Key,
+ description: 'Cache key used to define a cache affinity.'
+
+ node :untracked, Node::Boolean,
+ description: 'Cache all untracked files.'
+
+ node :paths, Node::Paths,
+ description: 'Specify which paths should be cached across builds.'
+
+ validations do
+ validates :config, allowed_keys: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
new file mode 100644
index 00000000000..37936fc8242
--- /dev/null
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This mixin is responsible for adding DSL, which purpose is to
+ # simplifly process of adding child nodes.
+ #
+ # This can be used only if parent node is a configuration entry that
+ # holds a hash as a configuration value, for example:
+ #
+ # job:
+ # script: ...
+ # artifacts: ...
+ #
+ module Configurable
+ extend ActiveSupport::Concern
+ include Validatable
+
+ included do
+ validations do
+ validates :config, type: Hash
+ end
+ end
+
+ private
+
+ def create_node(key, factory)
+ factory.with(value: @config[key], key: key, parent: self)
+
+ factory.create!
+ end
+
+ class_methods do
+ def nodes
+ Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
+ end
+
+ private
+
+ def node(symbol, entry_class, metadata)
+ factory = Node::Factory.new(entry_class)
+ .with(description: metadata[:description])
+
+ (@nodes ||= {}).merge!(symbol.to_sym => factory)
+ end
+
+ def helpers(*nodes)
+ nodes.each do |symbol|
+ define_method("#{symbol}_defined?") do
+ @nodes[symbol].try(:defined?)
+ end
+
+ define_method("#{symbol}_value") do
+ raise Entry::InvalidError unless valid?
+ @nodes[symbol].try(:value)
+ end
+
+ alias_method symbol.to_sym, "#{symbol}_value".to_sym
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
new file mode 100644
index 00000000000..9e79e170a4f
--- /dev/null
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -0,0 +1,92 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Base abstract class for each configuration entry node.
+ #
+ class Entry
+ class InvalidError < StandardError; end
+
+ attr_reader :config
+ attr_accessor :key, :parent, :description
+
+ def initialize(config)
+ @config = config
+ @nodes = {}
+ @validator = self.class.validator.new(self)
+ @validator.validate
+ end
+
+ def process!
+ return if leaf?
+ return unless valid?
+
+ compose!
+ process_nodes!
+ end
+
+ def nodes
+ @nodes.values
+ end
+
+ def leaf?
+ self.class.nodes.none?
+ end
+
+ def ancestors
+ @parent ? @parent.ancestors + [@parent] : []
+ end
+
+ def valid?
+ errors.none?
+ end
+
+ def errors
+ @validator.messages + nodes.flat_map(&:errors)
+ end
+
+ def value
+ if leaf?
+ @config
+ else
+ defined = @nodes.select { |_key, value| value.defined? }
+ Hash[defined.map { |key, node| [key, node.value] }]
+ end
+ end
+
+ def defined?
+ true
+ end
+
+ def self.default
+ end
+
+ def self.nodes
+ {}
+ end
+
+ def self.validator
+ Validator
+ end
+
+ private
+
+ def compose!
+ self.class.nodes.each do |key, essence|
+ @nodes[key] = create_node(key, essence)
+ end
+ end
+
+ def process_nodes!
+ nodes.each(&:process!)
+ end
+
+ def create_node(key, essence)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
new file mode 100644
index 00000000000..5919a283283
--- /dev/null
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Factory class responsible for fabricating node entry objects.
+ #
+ class Factory
+ class InvalidFactory < StandardError; end
+
+ def initialize(node)
+ @node = node
+ @attributes = {}
+ end
+
+ def with(attributes)
+ @attributes.merge!(attributes)
+ self
+ end
+
+ def create!
+ raise InvalidFactory unless @attributes.has_key?(:value)
+
+ fabricate.tap do |entry|
+ entry.key = @attributes[:key]
+ entry.parent = @attributes[:parent]
+ entry.description = @attributes[:description]
+ end
+ end
+
+ private
+
+ def fabricate
+ ##
+ # We assume that unspecified entry is undefined.
+ # See issue #18775.
+ #
+ if @attributes[:value].nil?
+ Node::Undefined.new(@node)
+ else
+ @node.new(@attributes[:value])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
new file mode 100644
index 00000000000..f92e1eccbcf
--- /dev/null
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents a global entry - root node for entire
+ # GitLab CI Configuration file.
+ #
+ class Global < Entry
+ include Configurable
+
+ node :before_script, Node::Script,
+ description: 'Script that will be executed before each job.'
+
+ node :image, Node::Image,
+ description: 'Docker image that will be used to execute jobs.'
+
+ node :services, Node::Services,
+ description: 'Docker images that will be linked to the container.'
+
+ node :after_script, Node::Script,
+ description: 'Script that will be executed after each job.'
+
+ node :variables, Node::Variables,
+ description: 'Environment variables that will be used.'
+
+ node :stages, Node::Stages,
+ description: 'Configuration of stages for this pipeline.'
+
+ node :types, Node::Stages,
+ description: 'Deprecated: stages for this pipeline.'
+
+ node :cache, Node::Cache,
+ description: 'Configure caching between build jobs.'
+
+ helpers :before_script, :image, :services, :after_script,
+ :variables, :stages, :types, :cache
+
+ def stages
+ stages_defined? ? stages_value : types_value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/image.rb b/lib/gitlab/ci/config/node/image.rb
new file mode 100644
index 00000000000..5d3c7c5eab0
--- /dev/null
+++ b/lib/gitlab/ci/config/node/image.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a Docker image.
+ #
+ class Image < Entry
+ include Validatable
+
+ validations do
+ validates :config, type: String
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/key.rb b/lib/gitlab/ci/config/node/key.rb
new file mode 100644
index 00000000000..f8b461ca098
--- /dev/null
+++ b/lib/gitlab/ci/config/node/key.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a key.
+ #
+ class Key < Entry
+ include Validatable
+
+ validations do
+ validates :config, key: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
new file mode 100644
index 00000000000..4d9a508796a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ module LegacyValidationHelpers
+ private
+
+ def validate_duration(value)
+ value.is_a?(String) && ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
+ end
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) &&
+ variables.all? { |key, value| validate_string(key) && validate_string(value) }
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
+ end
+
+ def validate_string_or_regexp(value)
+ return true if value.is_a?(Symbol)
+ return false unless value.is_a?(String)
+
+ if value.first == '/' && value.last == '/'
+ Regexp.new(value[1...-1])
+ else
+ true
+ end
+ rescue RegexpError
+ false
+ end
+
+ def validate_environment(value)
+ value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex
+ end
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/paths.rb b/lib/gitlab/ci/config/node/paths.rb
new file mode 100644
index 00000000000..3c6d3a52966
--- /dev/null
+++ b/lib/gitlab/ci/config/node/paths.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents an array of paths.
+ #
+ class Paths < Entry
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb
new file mode 100644
index 00000000000..39328f0fade
--- /dev/null
+++ b/lib/gitlab/ci/config/node/script.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a script.
+ #
+ class Script < Entry
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/services.rb b/lib/gitlab/ci/config/node/services.rb
new file mode 100644
index 00000000000..481e2b66adc
--- /dev/null
+++ b/lib/gitlab/ci/config/node/services.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a configuration of Docker services.
+ #
+ class Services < Entry
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/stages.rb b/lib/gitlab/ci/config/node/stages.rb
new file mode 100644
index 00000000000..b1fe45357ff
--- /dev/null
+++ b/lib/gitlab/ci/config/node/stages.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a configuration for pipeline stages.
+ #
+ class Stages < Entry
+ include Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+
+ def self.default
+ %w[build test deploy]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb
new file mode 100644
index 00000000000..699605e1e3a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/undefined.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents an undefined entry node.
+ #
+ # It takes original entry class as configuration and returns default
+ # value of original entry as self value.
+ #
+ #
+ class Undefined < Entry
+ include Validatable
+
+ validations do
+ validates :config, type: Class
+ end
+
+ def value
+ @config.default
+ end
+
+ def defined?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb
new file mode 100644
index 00000000000..f6e2896dfb2
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validatable.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ module Validatable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def validator
+ validator = Class.new(Node::Validator)
+
+ if defined?(@validations)
+ @validations.each { |rules| validator.class_eval(&rules) }
+ end
+
+ validator
+ end
+
+ private
+
+ def validations(&block)
+ (@validations ||= []).append(block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb
new file mode 100644
index 00000000000..758a6cf4356
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validator.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ class Validator < SimpleDelegator
+ include ActiveModel::Validations
+ include Node::Validators
+
+ def initialize(node)
+ super(node)
+ @node = node
+ end
+
+ def messages
+ errors.full_messages.map do |error|
+ "#{location} #{error}".downcase
+ end
+ end
+
+ def self.name
+ 'Validator'
+ end
+
+ def unknown_keys
+ return [] unless config.is_a?(Hash)
+
+ config.keys - @node.class.nodes.keys
+ end
+
+ private
+
+ def location
+ predecessors = ancestors.map(&:key).compact
+ current = key || @node.class.name.demodulize.underscore
+ predecessors.append(current).join(':')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb
new file mode 100644
index 00000000000..7b2f57990b5
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validators.rb
@@ -0,0 +1,70 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ module Validators
+ class AllowedKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if record.unknown_keys.any?
+ unknown_list = record.unknown_keys.join(', ')
+ record.errors.add(:config,
+ "contains unknown keys: #{unknown_list}")
+ end
+ end
+ end
+
+ class ArrayOfStringsValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array of strings')
+ end
+ end
+ end
+
+ class BooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_boolean(value)
+ record.errors.add(attribute, 'should be a boolean value')
+ end
+ end
+ end
+
+ class KeyValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_string(value)
+ record.errors.add(attribute, 'should be a string or symbol')
+ end
+ end
+ end
+
+ class TypeValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ type = options[:with]
+ raise unless type.is_a?(Class)
+
+ unless value.is_a?(type)
+ record.errors.add(attribute, "should be a #{type.name}")
+ end
+ end
+ end
+
+ class VariablesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_variables(value)
+ record.errors.add(attribute, 'should be a hash of key value pairs')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/variables.rb b/lib/gitlab/ci/config/node/variables.rb
new file mode 100644
index 00000000000..5f813f81f55
--- /dev/null
+++ b/lib/gitlab/ci/config/node/variables.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents environment variables.
+ #
+ class Variables < Entry
+ include Validatable
+
+ validations do
+ validates :config, variables: true
+ end
+
+ def self.default
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 85583dce9ee..9dc2602867e 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -19,7 +19,7 @@ module Gitlab
select('date(created_at) as date, count(id) as total_amount').
map(&:attributes)
- dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a
+ dates = (1.year.ago.to_date..Date.today).to_a
dates.each do |date|
date_id = date.to_time.to_i.to_s
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index f44d1b3a44e..ffc1814b29d 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -1,18 +1,26 @@
module Gitlab
module CurrentSettings
def current_application_settings
- key = :current_application_settings
-
- RequestStore.store[key] ||= begin
- settings = nil
+ if RequestStore.active?
+ RequestStore.fetch(:current_application_settings) { ensure_application_settings! }
+ else
+ ensure_application_settings!
+ end
+ end
- if connect_to_db?
+ def ensure_application_settings!
+ if connect_to_db?
+ begin
settings = ::ApplicationSetting.current
- settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
+ # In case Redis isn't running or the Redis UNIX socket file is not available
+ rescue ::Redis::BaseError, ::Errno::ENOENT
+ settings = ::ApplicationSetting.last
end
- settings || fake_application_settings
+ settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
+
+ settings || fake_application_settings
end
def fake_application_settings
@@ -22,20 +30,25 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
- sign_in_text: Settings.extra['sign_in_text'],
+ sign_in_text: nil,
+ after_sign_up_text: nil,
+ help_page_text: nil,
+ shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
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'],
+ import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
akismet_enabled: false,
repository_checks_enabled: true,
+ container_registry_token_expire_delay: 5,
+ user_default_external: false,
)
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 6f9da69983a..078609c86f1 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,21 +1,44 @@
module Gitlab
module Database
+ # The max value of INTEGER type is the same between MySQL and PostgreSQL:
+ # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
+ # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
+ MAX_INT_VALUE = 2147483647
+
def self.adapter_name
connection.adapter_name
end
def self.mysql?
- adapter_name.downcase == 'mysql2'
+ adapter_name.casecmp('mysql2').zero?
end
def self.postgresql?
- adapter_name.downcase == 'postgresql'
+ adapter_name.casecmp('postgresql').zero?
end
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
+ def self.nulls_last_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if Gitlab::Database.postgresql?
+ order << ' NULLS LAST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'ASC'
+ end
+
+ order
+ end
+
+ def self.random
+ Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ end
+
def true_value
if Gitlab::Database.postgresql?
"'t'"
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
new file mode 100644
index 00000000000..dec20d8659b
--- /dev/null
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -0,0 +1,158 @@
+module Gitlab
+ module Database
+ module MigrationHelpers
+ # Creates a new index, concurrently when supported
+ #
+ # On PostgreSQL this method creates an index concurrently, on MySQL this
+ # creates a regular index.
+ #
+ # Example:
+ #
+ # add_concurrent_index :users, :some_column
+ #
+ # See Rails' `add_index` for more info on the available arguments.
+ def add_concurrent_index(table_name, column_name, options = {})
+ if transaction_open?
+ raise 'add_concurrent_index can not be run inside a transaction, ' \
+ 'you can disable transactions by calling disable_ddl_transaction! ' \
+ 'in the body of your migration class'
+ end
+
+ if Database.postgresql?
+ options = options.merge({ algorithm: :concurrently })
+ end
+
+ add_index(table_name, column_name, options)
+ end
+
+ # Updates the value of a column in batches.
+ #
+ # This method updates the table in batches of 5% of the total row count.
+ # This method will continue updating rows until no rows remain.
+ #
+ # When given a block this method will yield two values to the block:
+ #
+ # 1. An instance of `Arel::Table` for the table that is being updated.
+ # 2. The query to run as an Arel object.
+ #
+ # By supplying a block one can add extra conditions to the queries being
+ # executed. Note that the same block is used for _all_ queries.
+ #
+ # Example:
+ #
+ # update_column_in_batches(:projects, :foo, 10) do |table, query|
+ # query.where(table[:some_column].eq('hello'))
+ # end
+ #
+ # This would result in this method updating only rows where
+ # `projects.some_column` equals "hello".
+ #
+ # table - The name of the table.
+ # column - The name of the column to update.
+ # value - The value for the column.
+ #
+ # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop
+ # determines this method to be too complex while there's no way to make it
+ # less "complex" without introducing extra methods (which actually will
+ # make things _more_ complex).
+ #
+ # rubocop: disable Metrics/AbcSize
+ def update_column_in_batches(table, column, value)
+ table = Arel::Table.new(table)
+
+ count_arel = table.project(Arel.star.count.as('count'))
+ count_arel = yield table, count_arel if block_given?
+
+ total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i
+
+ return if total == 0
+
+ # Update in batches of 5% until we run out of any rows to update.
+ batch_size = ((total / 100.0) * 5.0).ceil
+
+ start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
+ start_arel = yield table, start_arel if block_given?
+ start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i
+
+ loop do
+ stop_arel = table.project(table[:id]).
+ where(table[:id].gteq(start_id)).
+ order(table[:id].asc).
+ take(1).
+ skip(batch_size)
+
+ stop_arel = yield table, stop_arel if block_given?
+ stop_row = exec_query(stop_arel.to_sql).to_hash.first
+
+ update_arel = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(table).
+ set([[table[column], value]]).
+ where(table[:id].gteq(start_id))
+
+ if stop_row
+ stop_id = stop_row['id'].to_i
+ start_id = stop_id
+ update_arel = update_arel.where(table[:id].lt(stop_id))
+ end
+
+ update_arel = yield table, update_arel if block_given?
+
+ execute(update_arel.to_sql)
+
+ # There are no more rows left to update.
+ break unless stop_row
+ end
+ end
+
+ # Adds a column with a default value without locking an entire table.
+ #
+ # This method runs the following steps:
+ #
+ # 1. Add the column with a default value of NULL.
+ # 2. Change the default value of the column to the specified value.
+ # 3. Update all existing rows in batches.
+ # 4. Set a `NOT NULL` constraint on the column if desired (the default).
+ #
+ # These steps ensure a column can be added to a large and commonly used
+ # table without locking the entire table for the duration of the table
+ # modification.
+ #
+ # table - The name of the table to update.
+ # column - The name of the column to add.
+ # type - The column type (e.g. `:integer`).
+ # default - The default value for the column.
+ # allow_null - When set to `true` the column will allow NULL values, the
+ # default is to not allow NULL values.
+ #
+ # This method can also take a block which is passed directly to the
+ # `update_column_in_batches` method.
+ def add_column_with_default(table, column, type, default:, allow_null: false, &block)
+ if transaction_open?
+ raise 'add_column_with_default can not be run inside a transaction, ' \
+ 'you can disable transactions by calling disable_ddl_transaction! ' \
+ 'in the body of your migration class'
+ end
+
+ transaction do
+ add_column(table, column, type, default: nil)
+
+ # Changing the default before the update ensures any newly inserted
+ # rows already use the proper default value.
+ change_column_default(table, column, default)
+ end
+
+ begin
+ update_column_in_batches(table, column, default, &block)
+
+ change_column_null(table, column, false) unless allow_null
+ # We want to rescue _all_ exceptions here, even those that don't inherit
+ # from StandardError.
+ rescue Exception => error # rubocop: disable all
+ remove_column(table, column)
+
+ raise error
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
new file mode 100644
index 00000000000..8406ca4269c
--- /dev/null
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Diff
+ class DiffRefs
+ attr_reader :base_sha
+ attr_reader :start_sha
+ attr_reader :head_sha
+
+ def initialize(base_sha:, start_sha: base_sha, head_sha:)
+ @base_sha = base_sha
+ @start_sha = start_sha
+ @head_sha = head_sha
+ end
+
+ def ==(other)
+ other.is_a?(self.class) &&
+ base_sha == other.base_sha &&
+ start_sha == other.start_sha &&
+ head_sha == other.head_sha
+ end
+
+ # There is only one case in which we will have `start_sha` and `head_sha`,
+ # but not `base_sha`, which is when a diff is generated between an
+ # orphaned branch and another branch, which means there _is_ no base, but
+ # we're still able to highlight it, and to create diff notes, which are
+ # the primary things `DiffRefs` are used for.
+ # `DiffRefs` are "complete" when they have `start_sha` and `head_sha`,
+ # because `base_sha` can always be derived from this, to return an actual
+ # sha, or `nil`.
+ # We have `base_sha` directly available on `DiffRefs` because it's faster#
+ # than having to look it up in the repo every time.
+ def complete?
+ start_sha && head_sha
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index d2e85cabf72..b0c50edba59 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -1,47 +1,83 @@
module Gitlab
module Diff
class File
- attr_reader :diff, :diff_refs
+ attr_reader :diff, :repository, :diff_refs
delegate :new_file, :deleted_file, :renamed_file,
- :old_path, :new_path, to: :diff, prefix: false
+ :old_path, :new_path, :a_mode, :b_mode,
+ :submodule?, :too_large?, to: :diff, prefix: false
- def initialize(diff, diff_refs)
+ def initialize(diff, repository:, diff_refs: nil)
@diff = diff
+ @repository = repository
@diff_refs = diff_refs
end
+ def position(line)
+ return unless diff_refs
+
+ Position.new(
+ old_path: old_path,
+ new_path: new_path,
+ old_line: line.old_line,
+ new_line: line.new_line,
+ diff_refs: diff_refs
+ )
+ end
+
+ def line_code(line)
+ return if line.meta?
+
+ Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
+ end
+
+ def line_for_line_code(code)
+ diff_lines.find { |line| line_code(line) == code }
+ end
+
+ def line_for_position(pos)
+ diff_lines.find { |line| position(line) == pos }
+ end
+
+ def position_for_line_code(code)
+ line = line_for_line_code(code)
+ position(line) if line
+ end
+
+ def line_code_for_position(pos)
+ line = line_for_position(pos)
+ line_code(line) if line
+ end
+
+ def content_commit
+ return unless diff_refs
+
+ repository.commit(deleted_file ? old_ref : new_ref)
+ end
+
def old_ref
- diff_refs[0] if diff_refs
+ diff_refs.try(:base_sha)
end
def new_ref
- diff_refs[1] if diff_refs
+ diff_refs.try(:head_sha)
end
- # Array of Gitlab::DIff::Line objects
+ # Array of Gitlab::Diff::Line objects
def diff_lines
- @lines ||= parser.parse(raw_diff.each_line).to_a
- end
-
- def too_large?
- diff.too_large?
+ @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
end
def highlighted_diff_lines
- Gitlab::Diff::Highlight.new(self).highlight
+ @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end
def parallel_diff_lines
- Gitlab::Diff::ParallelDiff.new(self).parallelize
+ @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end
def mode_changed?
- !!(diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode)
- end
-
- def parser
- Gitlab::Diff::Parser.new
+ a_mode && b_mode && a_mode != b_mode
end
def raw_diff
@@ -53,17 +89,15 @@ module Gitlab
end
def prev_line(index)
- if index > 0
- diff_lines[index - 1]
- end
+ diff_lines[index - 1] if index > 0
+ end
+
+ def paths
+ [old_path, new_path].compact
end
def file_path
- if diff.new_path.present?
- diff.new_path
- elsif diff.old_path.present?
- diff.old_path
- end
+ new_path.presence || old_path
end
def added_lines
@@ -73,6 +107,21 @@ module Gitlab
def removed_lines
diff_lines.count(&:removed?)
end
+
+ def old_blob(commit = content_commit)
+ return unless commit
+
+ parent_id = commit.parent_id
+ return unless parent_id
+
+ repository.blob_at(parent_id, old_path)
+ end
+
+ def blob(commit = content_commit)
+ return unless commit
+
+ repository.blob_at(commit.id, file_path)
+ end
end
end
end
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 9429b3ff88d..649a265a02c 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -1,11 +1,13 @@
module Gitlab
module Diff
class Highlight
- attr_reader :diff_file, :diff_lines, :raw_lines
+ attr_reader :diff_file, :diff_lines, :raw_lines, :repository
delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff
- def initialize(diff_lines)
+ def initialize(diff_lines, repository: nil)
+ @repository = repository
+
if diff_lines.is_a?(Gitlab::Diff::File)
@diff_file = diff_lines
@diff_lines = @diff_file.diff_lines
@@ -19,7 +21,7 @@ module Gitlab
@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'
+ next diff_line if diff_line.meta?
rich_line = highlight_line(diff_line) || diff_line.text
@@ -40,12 +42,12 @@ module Gitlab
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
+ rich_line =
+ if diff_line.unchanged? || diff_line.added?
+ new_lines[diff_line.new_pos - 1]
+ elsif diff_line.removed?
+ 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.
@@ -58,19 +60,12 @@ module Gitlab
def old_lines
return unless diff_file
- @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old))
+ @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_ref, diff_old_path)
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]
+ @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_ref, diff_new_path)
end
end
end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index dccb717e95d..87a9b1e23ac 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -1,6 +1,11 @@
module Gitlab
module Diff
class InlineDiffMarker
+ MARKDOWN_SYMBOLS = {
+ addition: "+",
+ deletion: "-"
+ }
+
attr_accessor :raw_line, :rich_line
def initialize(raw_line, rich_line = raw_line)
@@ -8,7 +13,7 @@ module Gitlab
@rich_line = ERB::Util.html_escape(rich_line)
end
- def mark(line_inline_diffs)
+ def mark(line_inline_diffs, mode: nil, markdown: false)
return rich_line unless line_inline_diffs
marker_ranges = []
@@ -20,13 +25,22 @@ module Gitlab
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)
+ # Mark each range
+ marker_ranges.each_with_index do |range, index|
+ before_content =
+ if markdown
+ "{#{MARKDOWN_SYMBOLS[mode]}"
+ else
+ "<span class='#{html_class_names(marker_ranges, mode, index)}'>"
+ end
+ after_content =
+ if markdown
+ "#{MARKDOWN_SYMBOLS[mode]}}"
+ else
+ "</span>"
+ end
+ offset = insert_around_range(rich_line, range, before_content, after_content, offset)
end
rich_line.html_safe
@@ -34,6 +48,14 @@ module Gitlab
private
+ def html_class_names(marker_ranges, mode, index)
+ class_names = ["idiff"]
+ class_names << "left" if index == 0
+ class_names << "right" if index == marker_ranges.length - 1
+ class_names << mode if mode
+ class_names.join(" ")
+ end
+
# Mapping of character positions in the raw line, to the rich (highlighted) line
def position_mapping
@position_mapping ||= begin
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 03730b435ad..c6189d660c2 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -9,6 +9,18 @@ module Gitlab
@old_pos, @new_pos = old_pos, new_pos
end
+ def old_line
+ old_pos unless added? || meta?
+ end
+
+ def new_line
+ new_pos unless removed? || meta?
+ end
+
+ def unchanged?
+ type.nil?
+ end
+
def added?
type == 'new'
end
@@ -16,6 +28,10 @@ module Gitlab
def removed?
type == 'old'
end
+
+ def meta?
+ type == 'match' || type == 'nonewline'
+ end
end
end
end
diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb
new file mode 100644
index 00000000000..576a761423e
--- /dev/null
+++ b/lib/gitlab/diff/line_mapper.rb
@@ -0,0 +1,64 @@
+# When provided a diff for a specific file, maps old line numbers to new line
+# numbers and back, to find out where a specific line in a file was moved by the
+# changes.
+module Gitlab
+ module Diff
+ class LineMapper
+ attr_accessor :diff_file
+
+ def initialize(diff_file)
+ @diff_file = diff_file
+ end
+
+ # Find new line number for old line number.
+ def old_to_new(old_line)
+ map_line_number(old_line, from: :old_line, to: :new_line)
+ end
+
+ # Find old line number for new line number.
+ def new_to_old(new_line)
+ map_line_number(new_line, from: :new_line, to: :old_line)
+ end
+
+ private
+
+ def diff_lines
+ @diff_lines ||= @diff_file.diff_lines
+ end
+
+ # Find old/new line number based on its old/new counterpart line number.
+ def map_line_number(from_line, from:, to:)
+ # If no diff file could be found, the file wasn't changed, and the
+ # mapped line number is the same as the specified line number.
+ return from_line unless diff_file
+
+ # To find the mapped line number for the specified line number,
+ # we need to find:
+ # - The diff line with that exact line number, if it is in the diff context
+ # - The first diff line with a higher line number, if it falls between diff contexts
+ # - The last known diff line, if it falls after the last diff context
+ diff_line = diff_lines.find do |diff_line|
+ diff_from_line = diff_line.send(from)
+ diff_from_line && diff_from_line >= from_line
+ end
+ diff_line ||= diff_lines.last
+
+ # If no diff line could be found, the file wasn't changed, and the
+ # mapped line number is the same as the specified line number.
+ return from_line unless diff_line
+
+ diff_from_line = diff_line.send(from)
+ diff_to_line = diff_line.send(to)
+
+ # If the line was removed, there is no mapped line number.
+ return unless diff_to_line
+
+ # Because we may not have the diff line with the exact line number
+ # we were looking for, we need to adjust the mapped line number.
+ distance = diff_from_line - from_line
+
+ diff_to_line - distance
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb
index 74f9b3c050a..1c1fc148123 100644
--- a/lib/gitlab/diff/parallel_diff.rb
+++ b/lib/gitlab/diff/parallel_diff.rb
@@ -15,17 +15,19 @@ module Gitlab
highlighted_diff_lines.each do |line|
full_line = line.text
type = line.type
- line_code = generate_line_code(diff_file.file_path, line)
+ line_code = diff_file.line_code(line)
line_new = line.new_pos
line_old = line.old_pos
+ position = diff_file.position(line)
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)
+ full_next_line = next_line.text
+ next_line_code = diff_file.line_code(next_line)
next_type = next_line.type
- next_line = next_line.text
+ next_position = diff_file.position(next_line)
end
case type
@@ -37,12 +39,14 @@ module Gitlab
number: line_old,
text: full_line,
line_code: line_code,
+ position: position
},
right: {
type: type,
number: line_new,
text: full_line,
- line_code: line_code
+ line_code: line_code,
+ position: position
}
}
when 'old'
@@ -55,12 +59,14 @@ module Gitlab
number: line_old,
text: full_line,
line_code: line_code,
+ position: position
},
right: {
type: next_type,
number: line_new,
- text: next_line,
- line_code: next_line_code
+ text: full_next_line,
+ line_code: next_line_code,
+ position: next_position,
}
}
skip_next = true
@@ -73,12 +79,14 @@ module Gitlab
number: line_old,
text: full_line,
line_code: line_code,
+ position: position
},
right: {
type: next_type,
number: nil,
text: "",
- line_code: nil
+ line_code: nil,
+ position: nil
}
}
end
@@ -95,12 +103,14 @@ module Gitlab
number: nil,
text: "",
line_code: line_code,
+ position: position
},
right: {
type: type,
number: line_new,
text: full_line,
- line_code: line_code
+ line_code: line_code,
+ position: position
}
}
end
@@ -108,12 +118,6 @@ module Gitlab
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 d0815fc7eea..59a2367b65d 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -17,16 +17,16 @@ module Gitlab
Enumerator.new do |yielder|
@lines.each do |line|
next if filename?(line)
-
- full_line = line.gsub(/\n/, '')
-
+
+ full_line = line.delete("\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
+
+ 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
@@ -39,8 +39,7 @@ module Gitlab
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
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
new file mode 100644
index 00000000000..989fff8918e
--- /dev/null
+++ b/lib/gitlab/diff/position.rb
@@ -0,0 +1,155 @@
+# Defines a specific location, identified by paths and line numbers,
+# within a specific diff, identified by start, head and base commit ids.
+module Gitlab
+ module Diff
+ class Position
+ attr_reader :old_path
+ attr_reader :new_path
+ attr_reader :old_line
+ attr_reader :new_line
+ attr_reader :base_sha
+ attr_reader :start_sha
+ attr_reader :head_sha
+
+ def initialize(attrs = {})
+ @old_path = attrs[:old_path]
+ @new_path = attrs[:new_path]
+ @old_line = attrs[:old_line]
+ @new_line = attrs[:new_line]
+
+ if attrs[:diff_refs]
+ @base_sha = attrs[:diff_refs].base_sha
+ @start_sha = attrs[:diff_refs].start_sha
+ @head_sha = attrs[:diff_refs].head_sha
+ else
+ @base_sha = attrs[:base_sha]
+ @start_sha = attrs[:start_sha]
+ @head_sha = attrs[:head_sha]
+ end
+ end
+
+ # `Gitlab::Diff::Position` objects are stored as serialized attributes in
+ # `DiffNote`, which use YAML to encode and decode objects.
+ # `#init_with` and `#encode_with` can be used to customize the en/decoding
+ # behavior. In this case, we override these to prevent memoized instance
+ # variables like `@diff_file` and `@diff_line` from being serialized.
+ def init_with(coder)
+ initialize(coder['attributes'])
+
+ self
+ end
+
+ def encode_with(coder)
+ coder['attributes'] = self.to_h
+ end
+
+ def key
+ @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line]
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && key == other.key
+ end
+
+ def to_h
+ {
+ old_path: old_path,
+ new_path: new_path,
+ old_line: old_line,
+ new_line: new_line,
+ base_sha: base_sha,
+ start_sha: start_sha,
+ head_sha: head_sha
+ }
+ end
+
+ def inspect
+ %(#<#{self.class}:#{object_id} #{to_h}>)
+ end
+
+ def complete?
+ file_path.present? &&
+ (old_line || new_line) &&
+ diff_refs.complete?
+ end
+
+ def to_json
+ JSON.generate(self.to_h)
+ end
+
+ def type
+ if old_line && new_line
+ nil
+ elsif new_line
+ 'new'
+ else
+ 'old'
+ end
+ end
+
+ def unchanged?
+ type.nil?
+ end
+
+ def added?
+ type == 'new'
+ end
+
+ def removed?
+ type == 'old'
+ end
+
+ def paths
+ [old_path, new_path].compact.uniq
+ end
+
+ def file_path
+ new_path.presence || old_path
+ end
+
+ def diff_refs
+ @diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha)
+ end
+
+ def diff_file(repository)
+ @diff_file ||= begin
+ if RequestStore.active?
+ key = {
+ project_id: repository.project.id,
+ start_sha: start_sha,
+ head_sha: head_sha,
+ path: file_path
+ }
+
+ RequestStore.fetch(key) { find_diff_file(repository) }
+ else
+ find_diff_file(repository)
+ end
+ end
+ end
+
+ def diff_line(repository)
+ @diff_line ||= diff_file(repository).line_for_position(self)
+ end
+
+ def line_code(repository)
+ @line_code ||= diff_file(repository).line_code_for_position(self)
+ end
+
+ private
+
+ def find_diff_file(repository)
+ diffs = Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ ).diffs(paths: paths)
+
+ diff = diffs.first
+ return unless diff
+
+ Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
new file mode 100644
index 00000000000..4d04f867268
--- /dev/null
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -0,0 +1,168 @@
+# Finds the diff position in the new diff that corresponds to the same location
+# specified by the provided position in the old diff.
+module Gitlab
+ module Diff
+ class PositionTracer
+ attr_accessor :repository
+ attr_accessor :old_diff_refs
+ attr_accessor :new_diff_refs
+ attr_accessor :paths
+
+ def initialize(repository:, old_diff_refs:, new_diff_refs:, paths: nil)
+ @repository = repository
+ @old_diff_refs = old_diff_refs
+ @new_diff_refs = new_diff_refs
+ @paths = paths
+ end
+
+ def trace(old_position)
+ return unless old_diff_refs.complete? && new_diff_refs.complete?
+ return unless old_position.diff_refs == old_diff_refs
+
+ # Suppose we have an MR with source branch `feature` and target branch `master`.
+ # When the MR was created, the head of `master` was commit A, and the
+ # head of `feature` was commit B, resulting in the original diff A->B.
+ # Since creation, `master` was updated to C.
+ # Now `feature` is being updated to D, and the newly generated MR diff is C->D.
+ # It is possible that C and D are direct decendants of A and B respectively,
+ # but this isn't necessarily the case as rebases and merges come into play.
+ #
+ # Suppose we have a diff note on the original diff A->B. Now that the MR
+ # is updated, we need to find out what line in C->D corresponds to the
+ # line the note was originally created on, so that we can update the diff note's
+ # records and continue to display it in the right place in the diffs.
+ # If we cannot find this line in the new diff, this means the diff note is now
+ # outdated, and we will display that fact to the user.
+ #
+ # In the new diff, the file the diff note was originally created on may
+ # have been renamed, deleted or even created, if the file existed in A and B,
+ # but was removed in C, and restored in D.
+ #
+ # Every diff note stores a Position object that defines a specific location,
+ # identified by paths and line numbers, within a specific diff, identified
+ # by start, head and base commit ids.
+ #
+ # For diff notes for diff A->B, the position looks like this:
+ # Position
+ # base_sha - ID of commit A
+ # head_sha - ID of commit B
+ # old_path - path as of A (nil if file was newly created)
+ # new_path - path as of B (nil if file was deleted)
+ # old_line - line number as of A (nil if file was newly created)
+ # new_line - line number as of B (nil if file was deleted)
+ #
+ # We can easily update `base_sha` and `head_sha` to hold the IDs of commits C and D,
+ # but need to find the paths and line numbers as of C and D.
+ #
+ # If the file was unchanged or newly created in A->B, the path as of D can be found
+ # by generating diff B->D ("head to head"), finding the diff file with
+ # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`.
+ # The path as of C can be found by taking diff C->D, finding the diff file
+ # with that same `new_path` and taking `diff_file.old_path`.
+ # The line number as of D can be found by using the LineMapper on diff B->D
+ # and providing the line number as of B.
+ # The line number as of C can be found by using the LineMapper on diff C->D
+ # and providing the line number as of D.
+ #
+ # If the file was deleted in A->B, the path as of C can be found
+ # by generating diff A->C ("base to base"), finding the diff file with
+ # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`.
+ # The path as of D can be found by taking diff C->D, finding the diff file
+ # with that same `old_path` and taking `diff_file.new_path`.
+ # The line number as of C can be found by using the LineMapper on diff A->C
+ # and providing the line number as of A.
+ # The line number as of D can be found by using the LineMapper on diff C->D
+ # and providing the line number as of C.
+
+ results = nil
+ results ||= trace_added_line(old_position) if old_position.added? || old_position.unchanged?
+ results ||= trace_removed_line(old_position) if old_position.removed? || old_position.unchanged?
+
+ return unless results
+
+ file_diff, old_line, new_line = results
+
+ Position.new(
+ old_path: file_diff.old_path,
+ new_path: file_diff.new_path,
+ head_sha: new_diff_refs.head_sha,
+ start_sha: new_diff_refs.start_sha,
+ base_sha: new_diff_refs.base_sha,
+ old_line: old_line,
+ new_line: new_line
+ )
+ end
+
+ private
+
+ def trace_added_line(old_position)
+ file_path = old_position.new_path
+
+ return unless diff_head_to_head
+
+ file_head_to_head = diff_head_to_head.find { |diff_file| diff_file.old_path == file_path }
+
+ file_path = file_head_to_head.new_path if file_head_to_head
+
+ new_line = LineMapper.new(file_head_to_head).old_to_new(old_position.new_line)
+
+ return unless new_line
+
+ file_diff = new_diffs.find { |diff_file| diff_file.new_path == file_path }
+ return unless file_diff
+
+ old_line = LineMapper.new(file_diff).new_to_old(new_line)
+
+ [file_diff, old_line, new_line]
+ end
+
+ def trace_removed_line(old_position)
+ file_path = old_position.old_path
+
+ return unless diff_base_to_base
+
+ file_base_to_base = diff_base_to_base.find { |diff_file| diff_file.old_path == file_path }
+
+ file_path = file_base_to_base.old_path if file_base_to_base
+
+ old_line = LineMapper.new(file_base_to_base).old_to_new(old_position.old_line)
+
+ return unless old_line
+
+ file_diff = new_diffs.find { |diff_file| diff_file.old_path == file_path }
+ return unless file_diff
+
+ new_line = LineMapper.new(file_diff).old_to_new(old_line)
+
+ [file_diff, old_line, new_line]
+ end
+
+ def diff_base_to_base
+ @diff_base_to_base ||= diff_files(old_diff_refs.base_sha || old_diff_refs.start_sha, new_diff_refs.base_sha || new_diff_refs.start_sha)
+ end
+
+ def diff_head_to_head
+ @diff_head_to_head ||= diff_files(old_diff_refs.head_sha, new_diff_refs.head_sha)
+ end
+
+ def new_diffs
+ @new_diffs ||= diff_files(new_diff_refs.start_sha, new_diff_refs.head_sha, use_base: true)
+ end
+
+ def diff_files(start_sha, head_sha, use_base: false)
+ base_sha = self.repository.merge_base(start_sha, head_sha) || start_sha
+
+ diffs = self.repository.raw_repository.diff(
+ use_base ? base_sha : start_sha,
+ head_sha,
+ {},
+ *paths
+ )
+
+ diffs.decorate! do |diff|
+ Gitlab::Diff::File.new(diff, repository: self.repository)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 8f9be6cd9a3..97701b0cd42 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -2,22 +2,21 @@ module Gitlab
module Email
module Message
class RepositoryPush
- attr_accessor :recipient
attr_reader :author_id, :ref, :action
include Gitlab::Routing.url_helpers
+ include DiffHelper
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 = {})
+ def initialize(notify, project_id, opts = {})
raise ArgumentError, 'Missing options: author_id, ref, action' unless
opts[:author_id] && opts[:ref] && opts[:action]
@notify = notify
@project_id = project_id
- @recipient = recipient
@opts = opts.dup
@author_id = @opts.delete(:author_id)
@@ -34,11 +33,15 @@ module Gitlab
end
def commits
- @commits ||= (Commit.decorate(compare.commits, project) if compare)
+ return unless compare
+
+ @commits ||= Commit.decorate(compare.commits, project)
end
def diffs
- @diffs ||= (compare.diffs if compare)
+ return unless compare
+
+ @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository)
end
def diffs_count
@@ -49,6 +52,10 @@ module Gitlab
@opts[:compare]
end
+ def diff_refs
+ @opts[:diff_refs]
+ end
+
def compare_timeout
diffs.overflow? if diffs
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 97ef9851d71..1c671a7487b 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -104,15 +104,7 @@ module Gitlab
end
def create_note(reply)
- Notes::CreateService.new(
- sent_notification.project,
- sent_notification.recipient,
- note: reply,
- noteable_type: sent_notification.noteable_type,
- noteable_id: sent_notification.noteable_id,
- commit_id: sent_notification.commit_id,
- line_code: sent_notification.line_code
- ).execute
+ sent_notification.create_note(reply)
end
end
end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 6ed36b51f12..3411eb1d9ce 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -65,7 +65,7 @@ module Gitlab
(l =~ /On \w+ \d+,? \d+,?.*wrote:/)
# Headers on subsequent lines
- break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX }
+ break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
# Headers on the same line
break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
new file mode 100644
index 00000000000..b63213ae208
--- /dev/null
+++ b/lib/gitlab/emoji.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Emoji
+ extend self
+
+ def emojis
+ Gemojione.index.instance_variable_get(:@emoji_by_name)
+ end
+
+ def emojis_by_moji
+ Gemojione.index.instance_variable_get(:@emoji_by_moji)
+ end
+
+ def emojis_names
+ emojis.keys.sort
+ end
+
+ def emoji_filename(name)
+ emojis[name]["unicode"]
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
index 3840765db87..1918d5b208d 100644
--- a/lib/gitlab/fogbugz_import/project_creator.rb
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
name: repo.safe_name,
path: repo.path,
@@ -21,12 +21,9 @@ module Gitlab
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
import_type: 'fogbugz',
import_source: repo.name,
- import_url: Project::UNKNOWN_IMPORT_URL
+ import_url: Project::UNKNOWN_IMPORT_URL,
+ import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } }
).execute
-
- project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session })
-
- project
end
end
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 07b856ca64c..9b681e636c7 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -1,6 +1,7 @@
module Gitlab
module Git
class Hook
+ GL_PROTOCOL = 'web'.freeze
attr_reader :name, :repo_path, :path
def initialize(name, repo_path)
@@ -14,7 +15,7 @@ module Gitlab
end
def trigger(gl_id, oldrev, newrev, ref)
- return true unless exists?
+ return [true, nil] unless exists?
case name
when "pre-receive", "post-receive"
@@ -29,19 +30,20 @@ module Gitlab
def call_receive_hook(gl_id, oldrev, newrev, ref)
changes = [oldrev, newrev, ref].join(" ")
- # function will return true if succesful
exit_status = false
+ exit_message = nil
vars = {
'GL_ID' => gl_id,
- 'PWD' => repo_path
+ 'PWD' => repo_path,
+ 'GL_PROTOCOL' => GL_PROTOCOL
}
options = {
chdir: repo_path
}
- Open3.popen2(vars, path, options) do |stdin, _, wait_thr|
+ Open3.popen3(vars, path, options) do |stdin, stdout, stderr, wait_thr|
exit_status = true
stdin.sync = true
@@ -60,17 +62,24 @@ module Gitlab
unless wait_thr.value == 0
exit_status = false
+ exit_message = retrieve_error_message(stderr, stdout)
end
end
- exit_status
+ [exit_status, exit_message]
end
def call_update_hook(gl_id, oldrev, newrev, ref)
Dir.chdir(repo_path) do
- system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev)
+ stdout, stderr, status = Open3.capture3({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev)
+ [status.success?, stderr.presence || stdout]
end
end
+
+ def retrieve_error_message(stderr, stdout)
+ err_message = stderr.gets
+ err_message.blank? ? stdout.gets : err_message
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index d2a0e316cbe..7679c7e4bb8 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -3,11 +3,12 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
- attr_reader :actor, :project
+ attr_reader :actor, :project, :protocol
- def initialize(actor, project)
+ def initialize(actor, project, protocol)
@actor = actor
@project = project
+ @protocol = protocol
end
def user
@@ -49,6 +50,8 @@ module Gitlab
end
def check(cmd, changes = nil)
+ return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
+
unless actor
return build_status_object(false, "No user or key was provided.")
end
@@ -164,6 +167,10 @@ module Gitlab
Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev)
end
+ def protocol_allowed?
+ Gitlab::ProtocolAccess.allowed?(protocol)
+ end
+
private
def protected_branch_action(oldrev, newrev, branch_name)
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 202263c6742..72992baffd4 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -9,6 +9,10 @@ module Gitlab
@formatter = Gitlab::ImportFormatter.new
end
+ def create!
+ self.klass.create!(self.attributes)
+ end
+
private
def gl_user_id(github_id)
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
new file mode 100644
index 00000000000..7d2d545b84e
--- /dev/null
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module GithubImport
+ class BranchFormatter < BaseFormatter
+ delegate :repo, :sha, :ref, to: :raw_data
+
+ def exists?
+ branch_exists? && commit_exists?
+ end
+
+ def name
+ @name ||= exists? ? ref : "#{ref}-#{short_id}"
+ end
+
+ def valid?
+ repo.present?
+ end
+
+ private
+
+ def branch_exists?
+ project.repository.branch_exists?(ref)
+ end
+
+ def commit_exists?
+ project.repository.commit(sha).present?
+ end
+
+ def short_id
+ sha.to_s[0..7]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 67988ea3460..043f10d96a9 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -1,26 +1,42 @@
module Gitlab
module GithubImport
class Client
- attr_reader :client, :api
+ GITHUB_SAFE_REMAINING_REQUESTS = 100
+ GITHUB_SAFE_SLEEP_TIME = 500
+
+ attr_reader :access_token
def initialize(access_token)
- @client = ::OAuth2::Client.new(
+ @access_token = access_token
+
+ if access_token
+ ::Octokit.auto_paginate = false
+ end
+ end
+
+ def api
+ @api ||= ::Octokit::Client.new(
+ access_token: access_token,
+ api_endpoint: github_options[:site],
+ # If there is no config, we're connecting to github.com and we
+ # should verify ssl.
+ connection_options: {
+ ssl: { verify: config ? config['verify_ssl'] : true }
+ }
+ )
+ end
+
+ def client
+ unless config
+ raise Projects::ImportService::Error,
+ 'OAuth configuration for GitHub missing.'
+ end
+
+ @client ||= ::OAuth2::Client.new(
config.app_id,
config.app_secret,
github_options.merge(ssl: { verify: config['verify_ssl'] })
)
-
- if access_token
- ::Octokit.auto_paginate = true
-
- @api = ::Octokit::Client.new(
- access_token: access_token,
- api_endpoint: github_options[:site],
- connection_options: {
- ssl: { verify: config['verify_ssl'] }
- }
- )
- end
end
def authorize_url(redirect_uri)
@@ -36,7 +52,7 @@ module Gitlab
def method_missing(method, *args, &block)
if api.respond_to?(method)
- api.send(method, *args, &block)
+ request { api.send(method, *args, &block) }
else
super(method, *args, &block)
end
@@ -53,7 +69,39 @@ module Gitlab
end
def github_options
- config["args"]["client_options"].deep_symbolize_keys
+ if config
+ config["args"]["client_options"].deep_symbolize_keys
+ else
+ OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ end
+ end
+
+ def rate_limit
+ api.rate_limit!
+ end
+
+ def rate_limit_exceed?
+ rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ end
+
+ def rate_limit_sleep_time
+ rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
+ end
+
+ def request
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+
+ data = yield
+
+ last_response = api.last_response
+
+ while last_response.rels[:next]
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+ last_response = last_response.rels[:next].get
+ data.concat(last_response.data) if last_response.data.is_a?(Array)
+ end
+
+ data
end
end
end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 7d679eaec6a..2c1b94ef2cd 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -8,6 +8,7 @@ module Gitlab
commit_id: raw_data.commit_id,
line_code: line_code,
author_id: author_id,
+ type: type,
created_at: raw_data.created_at,
updated_at: raw_data.updated_at
}
@@ -53,6 +54,10 @@ module Gitlab
def note
formatter.author_line(author) + body
end
+
+ def type
+ 'LegacyDiffNote' if on_diff?
+ end
end
end
end
diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb
new file mode 100644
index 00000000000..db1fabaa18a
--- /dev/null
+++ b/lib/gitlab/github_import/hook_formatter.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module GithubImport
+ class HookFormatter
+ EVENTS = %w[* create delete pull_request push].freeze
+
+ attr_reader :raw
+
+ delegate :id, :name, :active, to: :raw
+
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def config
+ raw.config.attrs
+ end
+
+ def valid?
+ (EVENTS & raw.events).any? && active
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 0f9e3ee14ee..3932fcb1eda 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,12 +3,15 @@ module Gitlab
class Importer
include Gitlab::ShellAdapter
- attr_reader :project, :client
+ attr_reader :client, :project, :repo, :repo_url
def initialize(project)
- @project = project
- if import_data_credentials
- @client = Client.new(import_data_credentials[:user])
+ @project = project
+ @repo = project.import_source
+ @repo_url = project.import_url
+
+ if credentials
+ @client = Client.new(credentials[:user])
@formatter = Gitlab::ImportFormatter.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
@@ -22,14 +25,13 @@ module Gitlab
private
- def import_data_credentials
- @import_data_credentials ||= project.import_data.credentials if project.import_data
+ def credentials
+ @credentials ||= project.import_data.credentials if project.import_data
end
def import_labels
- client.labels(project.import_source).each do |raw_data|
- Label.create!(LabelFormatter.new(project, raw_data).attributes)
- end
+ labels = client.labels(repo, per_page: 100)
+ labels.each { |raw| LabelFormatter.new(project, raw).create! }
true
rescue ActiveRecord::RecordInvalid => e
@@ -37,9 +39,8 @@ module Gitlab
end
def import_milestones
- client.list_milestones(project.import_source, state: :all).each do |raw_data|
- Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes)
- end
+ milestones = client.milestones(repo, state: :all, per_page: 100)
+ milestones.each { |raw| MilestoneFormatter.new(project, raw).create! }
true
rescue ActiveRecord::RecordInvalid => e
@@ -47,18 +48,15 @@ module Gitlab
end
def import_issues
- client.list_issues(project.import_source, state: :all,
- sort: :created,
- direction: :asc).each do |raw_data|
- gh_issue = IssueFormatter.new(project, raw_data)
+ issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
- if gh_issue.valid?
- issue = Issue.create!(gh_issue.attributes)
- apply_labels(gh_issue.number, issue)
+ issues.each do |raw|
+ gh_issue = IssueFormatter.new(project, raw)
- if gh_issue.has_comments?
- import_comments(gh_issue.number, issue)
- end
+ if gh_issue.valid?
+ issue = gh_issue.create!
+ apply_labels(issue)
+ import_comments(issue) if gh_issue.has_comments?
end
end
@@ -68,29 +66,79 @@ module Gitlab
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)
-
- if merge_request.save
- apply_labels(pull_request.number, merge_request)
- import_comments(pull_request.number, merge_request)
- import_comments_on_diff(pull_request.number, merge_request)
- end
- end
+ disable_webhooks
+
+ pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
+ pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
+
+ source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] }
+ target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] }
+ branches_removed = source_branches_removed | target_branches_removed
+
+ restore_branches(branches_removed)
+
+ pull_requests.each do |pull_request|
+ merge_request = pull_request.create!
+ apply_labels(merge_request)
+ import_comments(merge_request)
+ import_comments_on_diff(merge_request)
end
true
rescue ActiveRecord::RecordInvalid => e
raise Projects::ImportService::Error, e.message
+ ensure
+ clean_up_restored_branches(branches_removed)
+ clean_up_disabled_webhooks
+ end
+
+ def disable_webhooks
+ update_webhooks(hooks, active: false)
+ end
+
+ def clean_up_disabled_webhooks
+ update_webhooks(hooks, active: true)
+ end
+
+ def update_webhooks(hooks, options)
+ hooks.each do |hook|
+ client.edit_hook(repo, hook.id, hook.name, hook.config, options)
+ end
+ end
+
+ def hooks
+ @hooks ||=
+ begin
+ client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
+
+ # The GitHub Repository Webhooks API returns 404 for users
+ # without admin access to the repository when listing hooks.
+ # In this case we just want to return gracefully instead of
+ # spitting out an error and stop the import process.
+ rescue Octokit::NotFound
+ []
+ end
+ end
+
+ def restore_branches(branches)
+ branches.each do |name, sha|
+ client.create_ref(repo, "refs/heads/#{name}", sha)
+ end
+
+ project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*')
+ end
+
+ def clean_up_restored_branches(branches)
+ branches.each do |name, _|
+ client.delete_ref(repo, "heads/#{name}")
+ project.repository.delete_branch(name) rescue Rugged::ReferenceError
+ end
+
+ project.repository.after_remove_branch
end
- def apply_labels(number, issuable)
- issue = client.issue(project.import_source, number)
+ def apply_labels(issuable)
+ issue = client.issue(repo, issuable.iid)
if issue.labels.count > 0
label_ids = issue.labels.map do |raw|
@@ -101,27 +149,27 @@ module Gitlab
end
end
- def import_comments(issue_number, noteable)
- comments = client.issue_comments(project.import_source, issue_number)
- create_comments(comments, noteable)
+ def import_comments(issuable)
+ comments = client.issue_comments(repo, issuable.iid, per_page: 100)
+ create_comments(issuable, comments)
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)
+ def import_comments_on_diff(merge_request)
+ comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100)
+ create_comments(merge_request, comments)
end
- def create_comments(comments, noteable)
- comments.each do |raw_data|
- comment = CommentFormatter.new(project, raw_data)
- noteable.notes.create!(comment.attributes)
+ def create_comments(issuable, comments)
+ comments.each do |raw|
+ comment = CommentFormatter.new(project, raw)
+ issuable.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)
+ gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.update_attribute(:wiki_enabled, true)
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index c8173913b4e..835ec858b35 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -20,6 +20,10 @@ module Gitlab
raw_data.comments > 0
end
+ def klass
+ Issue
+ end
+
def number
raw_data.number
end
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index c2b9d40b511..9f18244e7d7 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -9,6 +9,10 @@ module Gitlab
}
end
+ def klass
+ Label
+ end
+
private
def color
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index e91a7e328cf..53d4b3102d1 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -14,6 +14,10 @@ module Gitlab
}
end
+ def klass
+ Milestone
+ end
+
private
def number
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index d21b942ad4b..a4ea2210abd 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,15 +1,20 @@
module Gitlab
module GithubImport
class PullRequestFormatter < BaseFormatter
+ delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true
+ delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true
+
def attributes
{
iid: number,
title: raw_data.title,
description: description,
- source_project: source_project,
- source_branch: source_branch.name,
- target_project: target_project,
- target_branch: target_branch.name,
+ source_project: source_branch_project,
+ source_branch: source_branch_name,
+ source_branch_sha: source_branch_sha,
+ target_project: target_branch_project,
+ target_branch: target_branch_name,
+ target_branch_sha: target_branch_sha,
state: state,
milestone: milestone,
author_id: author_id,
@@ -19,12 +24,24 @@ module Gitlab
}
end
+ def klass
+ MergeRequest
+ end
+
def number
raw_data.number
end
def valid?
- !cross_project? && source_branch.present? && target_branch.present?
+ source_branch.valid? && target_branch.valid? && !cross_project?
+ end
+
+ def source_branch
+ @source_branch ||= BranchFormatter.new(project, raw_data.head)
+ end
+
+ def target_branch
+ @target_branch ||= BranchFormatter.new(project, raw_data.base)
end
private
@@ -52,7 +69,7 @@ module Gitlab
end
def cross_project?
- source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
+ source_branch_repo.id != target_branch_repo.id
end
def description
@@ -65,35 +82,10 @@ module Gitlab
end
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?
+ @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present?
'merged'
- when raw_data.state == 'closed'
+ elsif raw_data.state == 'closed'
'closed'
else
'opened'
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 96717b42bae..3f76ec97977 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -5,9 +5,9 @@ module Gitlab
def initialize(project)
@project = project
- credentials = import_data
- if credentials && credentials[:password]
- @client = Client.new(credentials[:password])
+ import_data = project.import_data
+ if import_data && import_data.credentials && import_data.credentials[:password]
+ @client = Client.new(import_data.credentials[:password])
@formatter = Gitlab::ImportFormatter.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
@@ -17,7 +17,7 @@ module Gitlab
def execute
project_identifier = CGI.escape(project.import_source)
- #Issues && Comments
+ # Issues && Comments
issues = client.issues(project_identifier)
issues.each do |issue|
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 77c33db4b59..3d0418261bb 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
name: repo["name"],
path: repo["path"],
@@ -22,8 +22,6 @@ module Gitlab
import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
).execute
-
- project
end
end
end
diff --git a/lib/gitlab/gl_id.rb b/lib/gitlab/gl_id.rb
new file mode 100644
index 00000000000..624fd00367e
--- /dev/null
+++ b/lib/gitlab/gl_id.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module GlId
+ def self.gl_id(user)
+ if user.present?
+ "user-#{user.id}"
+ else
+ ""
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index ab900b641c4..d4f12cb1df9 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -3,11 +3,11 @@ module Gitlab
def add_gon_variables
gon.api_version = API::API.version
gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
- gon.default_issues_tracker = Project.new.default_issue_tracker.to_param
gon.max_file_size = current_application_settings.max_attachment_size
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_shortcuts_path
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
+ gon.award_menu_url = emojis_path
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
index 0abb7a64c17..326cfcaa8af 100644
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
name: repo.name,
path: repo.name,
@@ -21,12 +21,9 @@ module Gitlab
visibility_level: Gitlab::VisibilityLevel::PUBLIC,
import_type: "google_code",
import_source: repo.name,
- import_url: repo.import_url
+ import_url: repo.import_url,
+ import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } }
).execute
-
- project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map })
-
- project
end
end
end
diff --git a/lib/gitlab/graphs/commits.rb b/lib/gitlab/graphs/commits.rb
index 2122339d2db..3caf9036459 100644
--- a/lib/gitlab/graphs/commits.rb
+++ b/lib/gitlab/graphs/commits.rb
@@ -18,7 +18,7 @@ module Gitlab
end
def commit_per_day
- @commit_per_day ||= (@commits.size.to_f / @duration).round(1)
+ @commit_per_day ||= @commits.size / (@duration + 1)
end
def collect_data
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 280120b0f9e..41296415e35 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -1,7 +1,7 @@
module Gitlab
class Highlight
- def self.highlight(blob_name, blob_content, nowrap: true, plain: false)
- new(blob_name, blob_content, nowrap: nowrap).
+ def self.highlight(blob_name, blob_content, repository: nil, nowrap: true, plain: false)
+ new(blob_name, blob_content, nowrap: nowrap, repository: repository).
highlight(blob_content, continue: false, plain: plain)
end
@@ -10,12 +10,21 @@ module Gitlab
return [] unless blob
blob.load_all_data!(repository)
- highlight(file_name, blob.data).lines.map!(&:html_safe)
+ highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
end
- def initialize(blob_name, blob_content, nowrap: true)
+ attr_reader :lexer
+ def initialize(blob_name, blob_content, repository: nil, nowrap: true)
+ @blob_name = blob_name
+ @blob_content = blob_content
+ @repository = repository
@formatter = rouge_formatter(nowrap: nowrap)
- @lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText
+
+ @lexer = custom_language || begin
+ Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
+ rescue Rouge::Lexer::AmbiguousGuess => e
+ e.alternatives.sort_by(&:tag).first
+ end
end
def highlight(text, continue: true, plain: false)
@@ -30,6 +39,14 @@ module Gitlab
private
+ def custom_language
+ language_name = @repository && @repository.gitattribute(@blob_name, 'gitlab-language')
+
+ return nil unless language_name
+
+ Rouge::Lexer.find_fancy(language_name)
+ end
+
def rouge_formatter(options = {})
options = options.reverse_merge(
nowrap: true,
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
new file mode 100644
index 00000000000..588647e5adb
--- /dev/null
+++ b/lib/gitlab/import_export.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module ImportExport
+ extend self
+
+ VERSION = '0.1.1'
+
+ def export_path(relative_path:)
+ File.join(storage_path, relative_path)
+ end
+
+ def storage_path
+ File.join(Settings.shared['path'], 'tmp/project_exports')
+ end
+
+ def project_filename
+ "project.json"
+ end
+
+ def project_bundle_filename
+ "project.bundle"
+ end
+
+ def config_file
+ Rails.root.join('lib/gitlab/import_export/import_export.yml')
+ end
+
+ def version_filename
+ 'VERSION'
+ end
+
+ def version
+ VERSION
+ end
+
+ def reset_tokens?
+ true
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
new file mode 100644
index 00000000000..d230de781d5
--- /dev/null
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ImportExport
+ class AttributesFinder
+
+ def initialize(included_attributes:, excluded_attributes:, methods:)
+ @included_attributes = included_attributes || {}
+ @excluded_attributes = excluded_attributes || {}
+ @methods = methods || {}
+ end
+
+ def find(model_object)
+ parsed_hash = find_attributes_only(model_object)
+ parsed_hash.empty? ? model_object : { model_object => parsed_hash }
+ end
+
+ def parse(model_object)
+ parsed_hash = find_attributes_only(model_object)
+ yield parsed_hash unless parsed_hash.empty?
+ end
+
+ def find_included(value)
+ key = key_from_hash(value)
+ @included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
+ end
+
+ def find_excluded(value)
+ key = key_from_hash(value)
+ @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
+ end
+
+ def find_method(value)
+ key = key_from_hash(value)
+ @methods[key].nil? ? {} : { methods: @methods[key] }
+ end
+
+ private
+
+ def find_attributes_only(value)
+ find_included(value).merge(find_excluded(value)).merge(find_method(value))
+ end
+
+ def key_from_hash(value)
+ value.is_a?(Hash) ? value.keys.first : value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
new file mode 100644
index 00000000000..2249904145c
--- /dev/null
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module ImportExport
+ module CommandLineUtil
+ def tar_czf(archive:, dir:)
+ tar_with_options(archive: archive, dir: dir, options: 'czf')
+ end
+
+ def untar_zxf(archive:, dir:)
+ untar_with_options(archive: archive, dir: dir, options: 'zxf')
+ end
+
+ def git_bundle(repo_path:, bundle_path:)
+ execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
+ end
+
+ def git_unbundle(repo_path:, bundle_path:)
+ execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
+ end
+
+ private
+
+ def tar_with_options(archive:, dir:, options:)
+ execute(%W(tar -#{options} #{archive} -C #{dir} .))
+ end
+
+ def untar_with_options(archive:, dir:, options:)
+ execute(%W(tar -#{options} #{archive} -C #{dir}))
+ end
+
+ def execute(cmd)
+ output, status = Gitlab::Popen.popen(cmd)
+ @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero?
+ status.zero?
+ end
+
+ def git_bin_path
+ Gitlab.config.git.bin_path
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
new file mode 100644
index 00000000000..e341c4d9cf8
--- /dev/null
+++ b/lib/gitlab/import_export/error.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module ImportExport
+ class Error < StandardError; end
+ end
+end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
new file mode 100644
index 00000000000..82d1e1805c5
--- /dev/null
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module ImportExport
+ class FileImporter
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def self.import(*args)
+ new(*args).import
+ end
+
+ def initialize(archive_file:, shared:)
+ @archive_file = archive_file
+ @shared = shared
+ end
+
+ def import
+ FileUtils.mkdir_p(@shared.export_path)
+ decompress_archive
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def decompress_archive
+ result = untar_zxf(archive: @archive_file, dir: @shared.export_path)
+
+ raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
new file mode 100644
index 00000000000..05f4ad527ac
--- /dev/null
+++ b/lib/gitlab/import_export/import_export.yml
@@ -0,0 +1,59 @@
+# Model relationships to be included in the project import/export
+project_tree:
+ - issues:
+ - :events
+ - notes:
+ - :author
+ - :events
+ - :labels
+ - milestones:
+ - :events
+ - snippets:
+ - notes:
+ :author
+ - :releases
+ - project_members:
+ - :user
+ - merge_requests:
+ - notes:
+ - :author
+ - :events
+ - :merge_request_diff
+ - :events
+ - pipelines:
+ - notes:
+ - :author
+ - :events
+ - :statuses
+ - :variables
+ - :triggers
+ - :deploy_keys
+ - :services
+ - :hooks
+ - :protected_branches
+
+# Only include the following attributes for the models specified.
+included_attributes:
+ project:
+ - :description
+ - :issues_enabled
+ - :merge_requests_enabled
+ - :wiki_enabled
+ - :snippets_enabled
+ - :visibility_level
+ - :archived
+ user:
+ - :id
+ - :email
+ - :username
+ author:
+ - :name
+
+# Do not include the following attributes for the models specified.
+excluded_attributes:
+ snippets:
+ - :expired_at
+
+methods:
+ statuses:
+ - :type \ No newline at end of file
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
new file mode 100644
index 00000000000..8f66f48cbfe
--- /dev/null
+++ b/lib/gitlab/import_export/importer.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module ImportExport
+ class Importer
+ def initialize(project)
+ @archive_file = project.import_source
+ @current_user = project.creator
+ @project = project
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace)
+ end
+
+ def execute
+ if import_file && check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
+ project_tree.restored_project
+ else
+ raise Projects::ImportService::Error.new(@shared.errors.join(', '))
+ end
+
+ remove_import_file
+ end
+
+ private
+
+ def import_file
+ Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
+ shared: @shared)
+ end
+
+ def check_version!
+ Gitlab::ImportExport::VersionChecker.check!(shared: @shared)
+ end
+
+ def project_tree
+ @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user,
+ shared: @shared,
+ project: @project)
+ end
+
+ def repo_restorer
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
+ shared: @shared,
+ project: project_tree.restored_project)
+ end
+
+ def wiki_restorer
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
+ shared: @shared,
+ project: ProjectWiki.new(project_tree.restored_project),
+ wiki: true)
+ end
+
+ def uploads_restorer
+ Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
+ end
+
+ def path_with_namespace
+ File.join(@project.namespace.path, @project.path)
+ end
+
+ def repo_path
+ File.join(@shared.export_path, 'project.bundle')
+ end
+
+ def wiki_repo_path
+ File.join(@shared.export_path, 'project.wiki.bundle')
+ end
+
+ def remove_import_file
+ FileUtils.rm_rf(@archive_file)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
new file mode 100644
index 00000000000..b459054c198
--- /dev/null
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module ImportExport
+ class MembersMapper
+ attr_reader :missing_author_ids
+
+ def initialize(exported_members:, user:, project:)
+ @exported_members = exported_members
+ @user = user
+ @project = project
+ @missing_author_ids = []
+
+ # This needs to run first, as second call would be from #map
+ # which means project members already exist.
+ ensure_default_member!
+ end
+
+ def map
+ @map ||=
+ begin
+ @exported_members.inject(missing_keys_tracking_hash) do |hash, member|
+ existing_user = User.where(find_project_user_query(member)).first
+ old_user_id = member['user']['id']
+ if existing_user && add_user_as_team_member(existing_user, member)
+ hash[old_user_id] = existing_user.id
+ end
+ hash
+ end
+ end
+ end
+
+ def default_user_id
+ @user.id
+ end
+
+ private
+
+ def missing_keys_tracking_hash
+ Hash.new do |_, key|
+ @missing_author_ids << key
+ default_user_id
+ end
+ end
+
+ def ensure_default_member!
+ ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
+ end
+
+ def add_user_as_team_member(existing_user, member)
+ member['user'] = existing_user
+
+ ProjectMember.create(member_hash(member)).persisted?
+ end
+
+ def member_hash(member)
+ member.except('id').merge(source_id: @project.id, importing: true)
+ end
+
+ def find_project_user_query(member)
+ user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email']))
+ end
+
+ def user_arel
+ @user_arel ||= User.arel_table
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_creator.rb b/lib/gitlab/import_export/project_creator.rb
new file mode 100644
index 00000000000..77bb3ca6581
--- /dev/null
+++ b/lib/gitlab/import_export/project_creator.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module ImportExport
+ class ProjectCreator
+ def initialize(namespace_id, current_user, file, project_path)
+ @namespace_id = namespace_id
+ @current_user = current_user
+ @file = file
+ @project_path = project_path
+ end
+
+ def execute
+ ::Projects::CreateService.new(
+ @current_user,
+ name: @project_path,
+ path: @project_path,
+ namespace_id: @namespace_id,
+ import_type: "gitlab_project",
+ import_source: @file
+ ).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
new file mode 100644
index 00000000000..0ac6ff01e3b
--- /dev/null
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -0,0 +1,104 @@
+module Gitlab
+ module ImportExport
+ class ProjectTreeRestorer
+ def initialize(user:, shared:, project:)
+ @path = File.join(shared.export_path, 'project.json')
+ @user = user
+ @shared = shared
+ @project = project
+ end
+
+ def restore
+ json = IO.read(@path)
+ @tree_hash = ActiveSupport::JSON.decode(json)
+ @project_members = @tree_hash.delete('project_members')
+ create_relations
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ def restored_project
+ @restored_project ||= restore_project
+ end
+
+ private
+
+ def members_mapper
+ @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
+ user: @user,
+ project: restored_project)
+ end
+
+ # Loops through the tree of models defined in import_export.yml and
+ # finds them in the imported JSON so they can be instantiated and saved
+ # in the DB. The structure and relationships between models are guessed from
+ # the configuration yaml file too.
+ # Finally, it updates each attribute in the newly imported project.
+ def create_relations
+ saved = []
+ default_relation_list.each do |relation|
+ next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present?
+
+ create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
+
+ relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
+ relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s])
+ saved << restored_project.update_attribute(relation_key, relation_hash)
+ end
+ saved.all?
+ end
+
+ def default_relation_list
+ Gitlab::ImportExport::Reader.new(shared: @shared).tree.reject do |model|
+ model.is_a?(Hash) && model[:project_members]
+ end
+ end
+
+ def restore_project
+ return @project unless @tree_hash
+
+ project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
+ @project.update(project_params)
+ @project
+ end
+
+ # Given a relation hash containing one or more models and its relationships,
+ # loops through each model and each object from a model type and
+ # and assigns its correspondent attributes hash from +tree_hash+
+ # Example:
+ # +relation_key+ issues, loops through the list of *issues* and for each individual
+ # issue, finds any subrelations such as notes, creates them and assign them back to the hash
+ def create_sub_relations(relation, tree_hash)
+ relation_key = relation.keys.first.to_s
+ tree_hash[relation_key].each do |relation_item|
+ relation.values.flatten.each do |sub_relation|
+ relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
+ relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
+ end
+ end
+ end
+
+ def assign_relation_hash(relation_item, sub_relation)
+ if sub_relation.is_a?(Hash)
+ relation_hash = relation_item[sub_relation.keys.first.to_s]
+ sub_relation = sub_relation.keys.first
+ else
+ relation_hash = relation_item[sub_relation.to_s]
+ end
+ [relation_hash, sub_relation]
+ end
+
+ def create_relation(relation, relation_hash_list)
+ relation_array = [relation_hash_list].flatten.map do |relation_hash|
+ Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
+ relation_hash: relation_hash.merge('project_id' => restored_project.id),
+ members_mapper: members_mapper,
+ user: @user)
+ end
+
+ relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
new file mode 100644
index 00000000000..9153088e966
--- /dev/null
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module ImportExport
+ class ProjectTreeSaver
+ attr_reader :full_path
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ @full_path = File.join(@shared.export_path, ImportExport.project_filename)
+ end
+
+ def save
+ FileUtils.mkdir_p(@shared.export_path)
+
+ File.write(full_path, project_json_tree)
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def project_json_tree
+ @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
new file mode 100644
index 00000000000..15f5dd31035
--- /dev/null
+++ b/lib/gitlab/import_export/reader.rb
@@ -0,0 +1,115 @@
+module Gitlab
+ module ImportExport
+ class Reader
+ attr_reader :tree
+
+ def initialize(shared:)
+ @shared = shared
+ config_hash = YAML.load_file(Gitlab::ImportExport.config_file).deep_symbolize_keys
+ @tree = config_hash[:project_tree]
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes],
+ excluded_attributes: config_hash[:excluded_attributes],
+ methods: config_hash[:methods])
+ end
+
+ # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+ # for outputting a project in JSON format, including its relations and sub relations.
+ def project_tree
+ @attributes_finder.find_included(:project).merge(include: build_hash(@tree))
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+ #
+ # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
+ def build_hash(model_list)
+ model_list.map do |model_objects|
+ if model_objects.is_a?(Hash)
+ build_json_config_hash(model_objects)
+ else
+ @attributes_finder.find(model_objects)
+ end
+ end
+ end
+
+ # Called when the model is actually a hash containing other relations (more models)
+ # Returns the config in the right format for calling +to_json+
+ # +model_object_hash+ - A model relationship such as:
+ # {:merge_requests=>[:merge_request_diff, :notes]}
+ def build_json_config_hash(model_object_hash)
+ @json_config_hash = {}
+
+ model_object_hash.values.flatten.each do |model_object|
+ current_key = model_object_hash.keys.first
+
+ @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash }
+
+ handle_model_object(current_key, model_object)
+ process_sub_model(current_key, model_object) if model_object.is_a?(Hash)
+ end
+ @json_config_hash
+ end
+
+ # If the model is a hash, process the sub_models, which could also be hashes
+ # If there is a list, add to an existing array, otherwise use hash syntax
+ # +current_key+ main model that will be a key in the hash
+ # +model_object+ model or list of models to include in the hash
+ def process_sub_model(current_key, model_object)
+ sub_model_json = build_json_config_hash(model_object).dup
+ @json_config_hash.slice!(current_key)
+
+ if @json_config_hash[current_key] && @json_config_hash[current_key][:include]
+ @json_config_hash[current_key][:include] << sub_model_json
+ else
+ @json_config_hash[current_key] = { include: sub_model_json }
+ end
+ end
+
+ # Creates or adds to an existing hash an individual model or list
+ # +current_key+ main model that will be a key in the hash
+ # +model_object+ model or list of models to include in the hash
+ def handle_model_object(current_key, model_object)
+ if @json_config_hash[current_key]
+ add_model_value(current_key, model_object)
+ else
+ create_model_value(current_key, model_object)
+ end
+ end
+
+ # Constructs a new hash that will hold the configuration for that particular object
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ def create_model_value(current_key, value)
+ parsed_hash = { include: value }
+
+ @attributes_finder.parse(value) do |hash|
+ parsed_hash = { include: hash_or_merge(value, hash) }
+ end
+ @json_config_hash[current_key] = parsed_hash
+ end
+
+ # Adds new model configuration to an existing hash with key +current_key+
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ def add_model_value(current_key, value)
+ @attributes_finder.parse(value) { |hash| value = { value => hash } }
+ old_values = @json_config_hash[current_key][:include]
+ @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
+ end
+
+ # Construct a new hash or merge with an existing one a model configuration
+ # This is to fulfil +to_json+ requirements.
+ # +value+ existing model to be included in the hash
+ # +hash+ hash containing configuration generated mainly from +@attributes_finder+
+ def hash_or_merge(value, hash)
+ value.is_a?(Hash) ? value.merge(hash) : { value => hash }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
new file mode 100644
index 00000000000..9824df3f274
--- /dev/null
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -0,0 +1,130 @@
+module Gitlab
+ module ImportExport
+ class RelationFactory
+ OVERRIDES = { snippets: :project_snippets,
+ pipelines: 'Ci::Pipeline',
+ statuses: 'commit_status',
+ variables: 'Ci::Variable',
+ triggers: 'Ci::Trigger',
+ builds: 'Ci::Build',
+ hooks: 'ProjectHook' }.freeze
+
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+
+ BUILD_MODELS = %w[Ci::Build commit_status].freeze
+
+ def self.create(*args)
+ new(*args).create
+ end
+
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+ @relation_name = OVERRIDES[relation_sym] || relation_sym
+ @relation_hash = relation_hash.except('id', 'noteable_id')
+ @members_mapper = members_mapper
+ @user = user
+ end
+
+ # Creates an object from an actual model with name "relation_sym" with params from
+ # the relation_hash, updating references with new object IDs, mapping users using
+ # the "members_mapper" object, also updating notes if required.
+ def create
+ set_note_author if @relation_name == :notes
+ update_user_references
+ update_project_references
+ reset_ci_tokens if @relation_name == 'Ci::Trigger'
+ @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
+
+ generate_imported_object
+ end
+
+ private
+
+ def update_user_references
+ USER_REFERENCES.each do |reference|
+ if @relation_hash[reference]
+ @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
+ end
+ end
+ end
+
+ # Sets the author for a note. If the user importing the project
+ # has admin access, an actual mapping with new project members
+ # will be used. Otherwise, a note stating the original author name
+ # is left.
+ def set_note_author
+ old_author_id = @relation_hash['author_id']
+
+ # Users with admin access can map users
+ @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id
+
+ author = @relation_hash.delete('author')
+
+ update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
+ end
+
+ def missing_author?(old_author_id)
+ !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
+ end
+
+ def missing_author_note(updated_at, author_name)
+ timestamp = updated_at.split('.').first
+ "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
+ end
+
+ def generate_imported_object
+ if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
+ trace = @relation_hash.delete('trace')
+ imported_object do |object|
+ object.trace = trace
+ object.commit_id = nil
+ end
+ else
+ imported_object
+ end
+ end
+
+ def update_project_references
+ project_id = @relation_hash.delete('project_id')
+
+ # project_id may not be part of the export, but we always need to populate it if required.
+ @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id')
+ @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
+ @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
+ @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id']
+
+ # If source and target are the same, populate them with the new project ID.
+ if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] &&
+ @relation_hash['target_project_id'] == @relation_hash['source_project_id']
+ @relation_hash['source_project_id'] = project_id
+ end
+ end
+
+ def reset_ci_tokens
+ return unless Gitlab::ImportExport.reset_tokens?
+
+ # If we import/export a project to the same instance, tokens will have to be reset.
+ @relation_hash['token'] = nil
+ end
+
+ def relation_class
+ @relation_class ||= @relation_name.to_s.classify.constantize
+ end
+
+ def imported_object
+ imported_object = relation_class.new(@relation_hash)
+ yield(imported_object) if block_given?
+ imported_object.importing = true if imported_object.respond_to?(:importing)
+ imported_object
+ end
+
+ def update_note_for_missing_author(author_name)
+ @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
+ @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name)
+ end
+
+ def admin_user?
+ @user.is_admin?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
new file mode 100644
index 00000000000..546dae4d122
--- /dev/null
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module ImportExport
+ class RepoRestorer
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(project:, shared:, path_to_bundle:, wiki: false)
+ @project = project
+ @path_to_bundle = path_to_bundle
+ @shared = shared
+ @wiki = wiki
+ end
+
+ def restore
+ return wiki? unless File.exist?(@path_to_bundle)
+
+ FileUtils.mkdir_p(path_to_repo)
+
+ git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def repos_path
+ Gitlab.config.gitlab_shell.repos_path
+ end
+
+ def path_to_repo
+ @project.repository.path_to_repo
+ end
+
+ def wiki?
+ @wiki
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
new file mode 100644
index 00000000000..cce43fe994b
--- /dev/null
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module ImportExport
+ class RepoSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ attr_reader :full_path
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ return false if @project.empty_repo?
+
+ @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename)
+ bundle_to_disk
+ end
+
+ private
+
+ def bundle_to_disk
+ FileUtils.mkdir_p(@shared.export_path)
+ git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ def path_to_repo
+ @project.repository.path_to_repo
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
new file mode 100644
index 00000000000..6a60b65071f
--- /dev/null
+++ b/lib/gitlab/import_export/saver.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ImportExport
+ class Saver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def self.save(*args)
+ new(*args).save
+ end
+
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def save
+ if compress_and_save
+ remove_export_path
+ Rails.logger.info("Saved project export #{archive_file}")
+ archive_file
+ else
+ @shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}"))
+ false
+ end
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def compress_and_save
+ tar_czf(archive: archive_file, dir: @shared.export_path)
+ end
+
+ def remove_export_path
+ FileUtils.rm_rf(@shared.export_path)
+ end
+
+ def archive_file
+ @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
new file mode 100644
index 00000000000..5d6de8bc475
--- /dev/null
+++ b/lib/gitlab/import_export/shared.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module ImportExport
+ class Shared
+ attr_reader :errors, :opts
+
+ def initialize(opts)
+ @opts = opts
+ @errors = []
+ end
+
+ def export_path
+ @export_path ||= Gitlab::ImportExport.export_path(relative_path: opts[:relative_path])
+ end
+
+ def error(error)
+ error_out(error.message, caller[0].dup)
+ @errors << error.message
+ # Debug:
+ Rails.logger.error(error.backtrace)
+ end
+
+ private
+
+ def error_out(message, caller)
+ Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb
new file mode 100644
index 00000000000..df19354b76e
--- /dev/null
+++ b/lib/gitlab/import_export/uploads_restorer.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module ImportExport
+ class UploadsRestorer < UploadsSaver
+ def restore
+ return true unless File.directory?(uploads_export_path)
+
+ copy_files(uploads_export_path, uploads_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
new file mode 100644
index 00000000000..d6f4fa57510
--- /dev/null
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module ImportExport
+ class UploadsSaver
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ return true unless File.directory?(uploads_path)
+
+ copy_files(uploads_path, uploads_export_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def copy_files(source, destination)
+ FileUtils.mkdir_p(destination)
+ FileUtils.copy_entry(source, destination)
+ true
+ end
+
+ def uploads_export_path
+ File.join(@shared.export_path, 'uploads')
+ end
+
+ def uploads_path
+ File.join(Rails.root.join('public/uploads'), @project.path_with_namespace)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
new file mode 100644
index 00000000000..abfc694b879
--- /dev/null
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module ImportExport
+ class VersionChecker
+ def self.check!(*args)
+ new(*args).check!
+ end
+
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def check!
+ version = File.open(version_file, &:readline)
+ verify_version!(version)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def version_file
+ File.join(@shared.export_path, Gitlab::ImportExport.version_filename)
+ end
+
+ def verify_version!(version)
+ if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
+ raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ else
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
new file mode 100644
index 00000000000..9b642d740b7
--- /dev/null
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module ImportExport
+ class VersionSaver
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def save
+ FileUtils.mkdir_p(@shared.export_path)
+
+ File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def version_file
+ File.join(@shared.export_path, Gitlab::ImportExport.version_filename)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
new file mode 100644
index 00000000000..1eedae39f8a
--- /dev/null
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module ImportExport
+ class WikiRepoSaver < RepoSaver
+ def save
+ @wiki = ProjectWiki.new(@project)
+ return true unless wiki_repository_exists? # it's okay to have no Wiki
+ bundle_to_disk(File.join(@shared.export_path, project_filename))
+ end
+
+ def bundle_to_disk(full_path)
+ FileUtils.mkdir_p(@shared.export_path)
+ git_bundle(repo_path: path_to_repo, bundle_path: full_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def project_filename
+ "project.wiki.bundle"
+ end
+
+ def path_to_repo
+ @wiki.repository.path_to_repo
+ end
+
+ def wiki_repository_exists?
+ File.exist?(@wiki.repository.path_to_repo) && !@wiki.repository.empty?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index ccfdfbe73e8..59a05411fe9 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -20,11 +20,10 @@ module Gitlab
'Gitorious.org' => 'gitorious',
'Google Code' => 'google_code',
'FogBugz' => 'fogbugz',
- 'Any repo by URL' => 'git',
+ 'Repo by URL' => 'git',
+ 'GitLab export' => 'gitlab_project'
}
end
-
end
-
end
end
diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb
index baf52ff750d..b75ae512d92 100644
--- a/lib/gitlab/key_fingerprint.rb
+++ b/lib/gitlab/key_fingerprint.rb
@@ -17,9 +17,9 @@ module Gitlab
file.rewind
cmd = []
- cmd.push *%W(ssh-keygen)
- cmd.push *%W(-E md5) if explicit_fingerprint_algorithm?
- cmd.push *%W(-lf #{file.path})
+ cmd.push('ssh-keygen')
+ cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
+ cmd.push('-lf', file.path)
cmd_output, cmd_status = popen(cmd, '/tmp')
end
@@ -39,7 +39,7 @@ module Gitlab
# OpenSSH 6.8 introduces a new default output format for fingerprints.
# Check the version and decide which command to use.
- version_output, version_status = popen(%W(ssh -V))
+ version_output, version_status = popen(%w(ssh -V))
return false unless version_status.zero?
version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/)
diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb
new file mode 100644
index 00000000000..2a659ae4c74
--- /dev/null
+++ b/lib/gitlab/lazy.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ # A class that can be wrapped around an expensive method call so it's only
+ # executed when actually needed.
+ #
+ # Usage:
+ #
+ # object = Gitlab::Lazy.new { some_expensive_work_here }
+ #
+ # object['foo']
+ # object.bar
+ class Lazy < BasicObject
+ def initialize(&block)
+ @block = block
+ end
+
+ def method_missing(name, *args, &block)
+ __evaluate__
+
+ @result.__send__(name, *args, &block)
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ __evaluate__
+
+ @result.respond_to?(name, include_private) || super
+ end
+
+ private
+
+ def __evaluate__
+ @result = @block.call unless defined?(@result)
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index aff7ccb157f..f9bb5775323 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -93,6 +93,7 @@ module Gitlab
end
protected
+
def base_config
Gitlab.config.ldap
end
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
index 9d9617761b3..811363405a8 100644
--- a/lib/gitlab/lfs/response.rb
+++ b/lib/gitlab/lfs/response.rb
@@ -1,11 +1,11 @@
module Gitlab
module Lfs
class Response
-
- def initialize(project, user, request)
+ def initialize(project, user, ci, request)
@origin_project = project
@project = storage_project(project)
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -189,7 +189,7 @@ module Gitlab
return render_not_enabled unless Gitlab.config.lfs.enabled
unless @project.public?
- return render_unauthorized unless @user
+ return render_unauthorized unless @user || @ci
return render_forbidden unless user_can_fetch?
end
@@ -210,7 +210,7 @@ module Gitlab
def user_can_fetch?
# Check user access against the project they used to initiate the pull
- @user.can?(:download_code, @origin_project)
+ @ci || @user.can?(:download_code, @origin_project)
end
def user_can_push?
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
index 78d02891102..69bd5e62305 100644
--- a/lib/gitlab/lfs/router.rb
+++ b/lib/gitlab/lfs/router.rb
@@ -1,9 +1,12 @@
module Gitlab
module Lfs
class Router
- def initialize(project, user, request)
+ attr_reader :project, :user, :ci, :request
+
+ def initialize(project, user, ci, request)
@project = project
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -80,7 +83,7 @@ module Gitlab
def lfs
return unless @project
- Gitlab::Lfs::Response.new(@project, @user, @request)
+ Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
end
def sanitize_tmp_filename(name)
diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb
index a5f767b134d..dda371e6554 100644
--- a/lib/gitlab/markup_helper.rb
+++ b/lib/gitlab/markup_helper.rb
@@ -40,7 +40,7 @@ module Gitlab
# Returns boolean
def plain?(filename)
filename.downcase.end_with?('.txt') ||
- filename.downcase == 'readme'
+ filename.casecmp('readme').zero?
end
def previewable?(filename)
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 708ef79f304..dcec7543c13 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -56,7 +56,7 @@ module Gitlab
end
end
- # Instruments all public methods of a module.
+ # Instruments all public and private methods of a module.
#
# This method optionally takes a block that can be used to determine if a
# method should be instrumented or not. The block is passed the receiving
@@ -65,7 +65,8 @@ module Gitlab
#
# mod - The module to instrument.
def self.instrument_methods(mod)
- mod.public_methods(false).each do |name|
+ methods = mod.methods(false) + mod.private_methods(false)
+ methods.each do |name|
method = mod.method(name)
if method.owner == mod.singleton_class
@@ -76,13 +77,14 @@ module Gitlab
end
end
- # Instruments all public instance methods of a module.
+ # Instruments all public and private instance methods of a module.
#
# See `instrument_methods` for more information.
#
# mod - The module to instrument.
def self.instrument_instance_methods(mod)
- mod.public_instance_methods(false).each do |name|
+ methods = mod.instance_methods(false) + mod.private_instance_methods(false)
+ methods.each do |name|
method = mod.instance_method(name)
if method.owner == mod
@@ -146,22 +148,8 @@ module Gitlab
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
- trans = Gitlab::Metrics::Instrumentation.transaction
-
- if trans
- start = Time.now
- retval = super
- 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})
- end
-
- retval
+ if trans = Gitlab::Metrics::Instrumentation.transaction
+ trans.measure_method(#{label.inspect}) { super }
else
super
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
new file mode 100644
index 00000000000..c048fe20ba7
--- /dev/null
+++ b/lib/gitlab/metrics/method_call.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Metrics
+ # Class for tracking timing information about method calls
+ class MethodCall
+ attr_reader :real_time, :cpu_time, :call_count
+
+ # name - The full name of the method (including namespace) such as
+ # `User#sign_in`.
+ #
+ # series - The series to use for storing the data.
+ def initialize(name, series)
+ @name = name
+ @series = series
+ @real_time = 0.0
+ @cpu_time = 0.0
+ @call_count = 0
+ end
+
+ # Measures the real and CPU execution time of the supplied block.
+ def measure
+ start_real = System.monotonic_time
+ start_cpu = System.cpu_time
+ retval = yield
+
+ @real_time += System.monotonic_time - start_real
+ @cpu_time += System.cpu_time - start_cpu
+ @call_count += 1
+
+ retval
+ end
+
+ # Returns a Metric instance of the current method call.
+ def to_metric
+ Metric.new(
+ @series,
+ {
+ duration: real_time,
+ cpu_duration: cpu_time,
+ call_count: call_count
+ },
+ method: @name
+ )
+ end
+
+ # Returns true if the total runtime of this method exceeds the method call
+ # threshold.
+ def above_threshold?
+ real_time >= Metrics.method_call_threshold
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index 1cd1ca30f70..f23d67e1e38 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,16 +4,15 @@ module Gitlab
class Metric
JITTER_RANGE = 0.000001..0.001
- attr_reader :series, :values, :tags, :created_at
+ attr_reader :series, :values, :tags
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
def initialize(series, values, tags = {})
- @values = values
- @series = series
- @tags = tags
- @created_at = Time.now.utc
+ @values = values
+ @series = series
+ @tags = tags
end
# Returns a Hash in a format that can be directly written to InfluxDB.
@@ -27,20 +26,20 @@ module Gitlab
#
# Due to the way InfluxDB is set up there's no solution to this problem,
# all we can do is lower the amount of collisions. We do this by using
- # Time#to_f which returns the seconds as a Float providing greater
- # accuracy. We then add a small random value that is large enough to
- # distinguish most timestamps but small enough to not alter the amount
- # of seconds.
+ # System.real_time which returns the nanoseconds as a Float providing
+ # greater accuracy. We then add a small random value that is large
+ # enough to distinguish most timestamps but small enough to not alter
+ # the timestamp significantly.
#
# See https://gitlab.com/gitlab-com/operations/issues/175 for more
# information.
- time = @created_at.to_f + rand(JITTER_RANGE)
+ time = System.real_time(:nanosecond) + rand(JITTER_RANGE)
{
series: @series,
tags: @tags,
values: @values,
- timestamp: (time * 1_000_000_000).to_i
+ timestamp: time.to_i
}
end
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 6f179789d3e..e61670f491c 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -1,8 +1,9 @@
module Gitlab
module Metrics
- # Rack middleware for tracking Rails requests.
+ # Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
+ ENDPOINT_KEY = 'api.endpoint'
def initialize(app)
@app = app
@@ -21,6 +22,8 @@ module Gitlab
ensure
if env[CONTROLLER_KEY]
tag_controller(trans, env)
+ elsif env[ENDPOINT_KEY]
+ tag_endpoint(trans, env)
end
trans.finish
@@ -32,7 +35,7 @@ module Gitlab
def transaction_from_env(env)
trans = Transaction.new
- trans.set(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_uri, filtered_path(env))
trans.set(:request_method, env['REQUEST_METHOD'])
trans
@@ -42,6 +45,30 @@ module Gitlab
controller = env[CONTROLLER_KEY]
trans.action = "#{controller.class.name}##{controller.action_name}"
end
+
+ def tag_endpoint(trans, env)
+ endpoint = env[ENDPOINT_KEY]
+ path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
+ trans.action = "Grape##{endpoint.route.route_method} #{path}"
+ end
+
+ private
+
+ def filtered_path(env)
+ ActionDispatch::Request.new(env).filtered_path.presence || env['REQUEST_URI']
+ end
+
+ def endpoint_paths_cache
+ @endpoint_paths_cache ||= Hash.new do |hash, http_method|
+ hash[http_method] = Hash.new do |inner_hash, raw_path|
+ inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
+ end
+ end
+ end
+
+ def endpoint_instrumentable_path(raw_path)
+ raw_path.sub('(.:format)', '').sub('/:version', '')
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
index fc709222a9b..0000450d9bb 100644
--- a/lib/gitlab/metrics/sampler.rb
+++ b/lib/gitlab/metrics/sampler.rb
@@ -66,7 +66,11 @@ module Gitlab
def sample_objects
sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash|
- hash[klass.name] = count
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
end
# Symbols aren't allocated so we'll need to add those manually.
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index fd98aa3412e..a1240fd33ee 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -8,6 +8,8 @@ module Gitlab
trans = Transaction.new("#{worker.class.name}#perform")
begin
+ # Old gitlad-shell messages don't provide enqueued_at/created_at attributes
+ trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
ensure
trans.finish
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index 49e5f86e6e6..aaed2184f44 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -2,30 +2,54 @@ module Gitlab
module Metrics
module Subscribers
# Class for tracking the total time spent in Rails cache calls
+ # http://guides.rubyonrails.org/active_support_instrumentation.html
class RailsCache < ActiveSupport::Subscriber
attach_to :active_support
def cache_read(event)
- increment(:cache_read_duration, event.duration)
+ increment(:cache_read, event.duration)
+
+ return unless current_transaction
+ return if event.payload[:super_operation] == :fetch
+
+ if event.payload[:hit]
+ current_transaction.increment(:cache_read_hit_count, 1)
+ else
+ current_transaction.increment(:cache_read_miss_count, 1)
+ end
end
def cache_write(event)
- increment(:cache_write_duration, event.duration)
+ increment(:cache_write, event.duration)
end
def cache_delete(event)
- increment(:cache_delete_duration, event.duration)
+ increment(:cache_delete, event.duration)
end
def cache_exist?(event)
- increment(:cache_exists_duration, event.duration)
+ increment(:cache_exists, event.duration)
+ end
+
+ def cache_fetch_hit(event)
+ return unless current_transaction
+
+ current_transaction.increment(:cache_read_hit_count, 1)
+ end
+
+ def cache_generate(event)
+ return unless current_transaction
+
+ current_transaction.increment(:cache_read_miss_count, 1)
end
def increment(key, duration)
return unless current_transaction
current_transaction.increment(:cache_duration, duration)
- current_transaction.increment(key, duration)
+ current_transaction.increment(:cache_count, 1)
+ current_transaction.increment("#{key}_duration".to_sym, duration)
+ current_transaction.increment("#{key}_count".to_sym, 1)
end
private
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index a7d183b2f94..82c18bb108b 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -34,13 +34,29 @@ module Gitlab
# THREAD_CPUTIME is not supported on OS X
if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)
def self.cpu_time
- Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
+ Process.
+ clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond).to_f
end
else
def self.cpu_time
- Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond)
+ Process.
+ clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond).to_f
end
end
+
+ # Returns the current real time in a given precision.
+ #
+ # Returns the time as a Float.
+ def self.real_time(precision = :millisecond)
+ Process.clock_gettime(Process::CLOCK_REALTIME, precision).to_f
+ end
+
+ # Returns the current monotonic clock time in a given precision.
+ #
+ # Returns the time as a Float.
+ def self.monotonic_time(precision = :millisecond)
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, precision).to_f
+ end
end
end
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 2578ddc49f4..bded245da43 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,7 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- attr_reader :tags, :values
+ attr_reader :tags, :values, :methods
attr_accessor :action
@@ -16,6 +16,7 @@ module Gitlab
# plus method name.
def initialize(action = nil)
@metrics = []
+ @methods = {}
@started_at = nil
@finished_at = nil
@@ -29,7 +30,7 @@ module Gitlab
end
def duration
- @finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
+ @finished_at ? (@finished_at - @started_at) : 0.0
end
def allocated_memory
@@ -40,20 +41,34 @@ module Gitlab
Thread.current[THREAD_KEY] = self
@memory_before = System.memory_usage
- @started_at = Time.now
+ @started_at = System.monotonic_time
yield
ensure
@memory_after = System.memory_usage
- @finished_at = Time.now
+ @finished_at = System.monotonic_time
Thread.current[THREAD_KEY] = nil
end
def add_metric(series, values, tags = {})
- prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+ @metrics << Metric.new("#{series_prefix}#{series}", values, tags)
+ end
+
+ # Measures the time it takes to execute a method.
+ #
+ # Multiple calls to the same method add up to the total runtime of the
+ # method.
+ #
+ # name - The full name of the method to measure (e.g. `User#sign_in`).
+ def measure_method(name, &block)
+ unless @methods[name]
+ series = "#{series_prefix}#{Instrumentation::SERIES}"
+
+ @methods[name] = MethodCall.new(name, series)
+ end
- @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ @methods[name].measure(&block)
end
def increment(name, value)
@@ -84,7 +99,13 @@ module Gitlab
end
def submit
- metrics = @metrics.map do |metric|
+ submit = @metrics.dup
+
+ @methods.each do |name, method|
+ submit << method.to_metric if method.above_threshold?
+ end
+
+ submit_hashes = submit.map do |metric|
hash = metric.to_hash
hash[:tags][:action] ||= @action if @action
@@ -92,12 +113,16 @@ module Gitlab
hash
end
- Metrics.submit_metrics(metrics)
+ Metrics.submit_metrics(submit_hashes)
end
def sidekiq?
Sidekiq.server?
end
+
+ def series_prefix
+ sidekiq? ? 'sidekiq_' : 'rails_'
+ end
end
end
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 50b0dd32380..5764ab15652 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -39,7 +39,7 @@ module Gitlab
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";
+ "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"
end
def strip_url(url)
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
new file mode 100644
index 00000000000..56608b1b276
--- /dev/null
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -0,0 +1,24 @@
+# This Rack middleware is intended to measure the latency between
+# gitlab-workhorse forwarding a request to the Rails application and the
+# time this middleware is reached.
+
+module Gitlab
+ module Middleware
+ class RailsQueueDuration
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ trans = Gitlab::Metrics.current_transaction
+ proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence
+ if trans && proxy_start
+ # Time in milliseconds since gitlab-workhorse started the request
+ trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
+ end
+
+ @app.call(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 36e5c2670bb..7d6911a1ab3 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -66,7 +66,7 @@ module Gitlab
# Get the first part of the email address (before @)
# In addtion in removes illegal characters
def generate_username(email)
- email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/,'').to_s
+ email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
end
def generate_temporarily_email(username)
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 356e96fcbab..0a91d3918d5 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -56,8 +56,6 @@ module Gitlab
if external_provider? && @user
@user.external = true
- elsif @user
- @user.external = false
end
@user
@@ -69,13 +67,20 @@ module Gitlab
return unless ldap_person
# If a corresponding person exists with same uid in a LDAP server,
- # set up a Gitlab user with dual LDAP and Omniauth identities.
- if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
- # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account.
- user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider)
+ # check if the user already has a GitLab account.
+ user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
+ if user
+ # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account.
+ log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
+ user.identities.find_or_initialize_by(extern_uid: auth_hash.uid, provider: auth_hash.provider)
else
- # No account in Gitlab yet: create it and add the LDAP identity
- user = build_new_user
+ log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account."
+ user = find_by_uid_and_provider
+ if user.nil?
+ log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
+ user = build_new_user
+ end
+ log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}."
user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn)
end
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
index 746ec283330..4e2f8ed5587 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -1,7 +1,6 @@
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
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 71c5b6801fb..183bd10d6a3 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -74,7 +74,7 @@ module Gitlab
end
def notes
- project.notes.user.search(query).order('updated_at DESC')
+ project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
end
def commits
diff --git a/lib/gitlab/protocol_access.rb b/lib/gitlab/protocol_access.rb
new file mode 100644
index 00000000000..21aefc884be
--- /dev/null
+++ b/lib/gitlab/protocol_access.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ProtocolAccess
+ def self.allowed?(protocol)
+ if protocol == 'web'
+ true
+ elsif current_application_settings.enabled_git_access_protocol.blank?
+ true
+ else
+ protocol == current_application_settings.enabled_git_access_protocol
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 5c352c96de5..40766f35f77 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -25,7 +25,7 @@ module Gitlab
end
@pool.with { |redis| yield redis }
end
-
+
def self.redis_store_options
url = new.url
redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url)
@@ -40,10 +40,10 @@ module Gitlab
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]
+ if File.exist?(config_file)
+ @url = YAML.load_file(config_file)[rails_env]
end
end
end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 13c4d64c99b..11c0b01f0dc 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -4,10 +4,9 @@ module Gitlab
REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
attr_accessor :project, :current_user, :author
- def initialize(project, current_user = nil, author = nil)
+ def initialize(project, current_user = nil)
@project = project
@current_user = current_user
- @author = author
@references = {}
@@ -18,17 +17,21 @@ module Gitlab
super(text, context.merge(project: project))
end
+ def references(type)
+ super(type, project, current_user)
+ end
+
REFERABLES.each do |type|
define_method("#{type}s") do
- @references[type] ||= references(type, reference_context)
+ @references[type] ||= references(type)
end
end
def issues
if project && project.jira_tracker?
- @references[:external_issue] ||= references(:external_issue, reference_context)
+ @references[:external_issue] ||= references(:external_issue)
else
- @references[:issue] ||= references(:issue, reference_context)
+ @references[:issue] ||= references(:issue)
end
end
@@ -46,11 +49,5 @@ module Gitlab
@pattern = Regexp.union(patterns.compact)
end
-
- private
-
- def reference_context
- { project: project, current_user: current_user, author: author }
- end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index ace906a6f59..ffad5e17c78 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -13,7 +13,6 @@ module Gitlab
"Cannot start with '-' or end in '.'." \
end
-
def namespace_name_regex
@namespace_name_regex ||= /\A[\p{Alnum}\p{Pd}_\. ]*\z/.freeze
end
@@ -22,7 +21,6 @@ module Gitlab
"can contain only letters, digits, '_', '.', dash and space."
end
-
def project_name_regex
@project_name_regex ||= /\A[\p{Alnum}_][\p{Alnum}\p{Pd}_\. ]*\z/.freeze
end
@@ -32,7 +30,6 @@ module Gitlab
"It must start with letter, digit or '_'."
end
-
def project_path_regex
@project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git|\.atom)\z/.freeze
end
@@ -42,7 +39,6 @@ module Gitlab
"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
end
@@ -59,7 +55,6 @@ module Gitlab
"can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'. "
end
-
def directory_traversal_regex
@directory_traversal_regex ||= /\.{2}/.freeze
end
@@ -68,7 +63,6 @@ module Gitlab
"cannot include directory traversal. "
end
-
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
@archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
@@ -96,5 +90,17 @@ module Gitlab
(?<![\/.]) (?# rule #6-7)
}x.freeze
end
+
+ def container_registry_reference_regex
+ git_reference_regex
+ end
+
+ def environment_name_regex
+ @environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze
+ end
+
+ def environment_name_regex_message
+ "can contain only letters, digits, '-' and '_'."
+ end
end
end
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
index 32c1c9ec5bb..67a5f368bdb 100644
--- a/lib/gitlab/saml/auth_hash.rb
+++ b/lib/gitlab/saml/auth_hash.rb
@@ -1,7 +1,6 @@
module Gitlab
module Saml
class AuthHash < Gitlab::OAuth::AuthHash
-
def groups
get_raw(Gitlab::Saml::Config.groups)
end
@@ -13,7 +12,6 @@ module Gitlab
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
-
end
end
end
diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb
index 0f40c00f547..574c3a4b28c 100644
--- a/lib/gitlab/saml/config.rb
+++ b/lib/gitlab/saml/config.rb
@@ -1,7 +1,6 @@
module Gitlab
module Saml
class Config
-
class << self
def options
Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
@@ -15,7 +14,6 @@ module Gitlab
options[:external_groups]
end
end
-
end
end
end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index dba4bbfc899..f253dc7477e 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -6,18 +6,17 @@
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
+ @user ||= find_by_uid_and_provider
+
if auto_link_saml_user?
@user ||= find_by_email
end
diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb
index b98589dff89..8304b9a482c 100644
--- a/lib/gitlab/sanitizers/svg.rb
+++ b/lib/gitlab/sanitizers/svg.rb
@@ -1,5 +1,3 @@
-require_relative "svg/whitelist"
-
module Gitlab
module Sanitizers
module SVG
@@ -12,25 +10,47 @@ module Gitlab
DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u
def scrub(node)
- unless ALLOWED_ELEMENTS.include?(node.name)
+ unless Whitelist::ALLOWED_ELEMENTS.include?(node.name)
node.unlink
- else
- node.attributes.each do |attr_name, attr|
- valid_attributes = ALLOWED_ATTRIBUTES[node.name]
-
- unless valid_attributes && valid_attributes.include?(attr_name)
- if ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) &&
- attr_name.start_with?('data-')
- # Arbitrary data attributes are allowed. Verify that the attribute
- # is a valid data attribute.
- attr.unlink unless attr_name =~ DATA_ATTR_PATTERN
- else
- attr.unlink
- end
+ return
+ end
+
+ valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name]
+ return unless valid_attributes
+
+ node.attribute_nodes.each do |attr|
+ attr_name = attribute_name_with_namespace(attr)
+
+ if valid_attributes.include?(attr_name)
+ attr.unlink if unsafe_href?(attr)
+ else
+ # Arbitrary data attributes are allowed.
+ unless allows_data_attribute?(node) && data_attribute?(attr)
+ attr.unlink
end
end
end
end
+
+ def attribute_name_with_namespace(attr)
+ if attr.namespace
+ "#{attr.namespace.prefix}:#{attr.name}"
+ else
+ attr.name
+ end
+ end
+
+ def allows_data_attribute?(node)
+ Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name)
+ end
+
+ def unsafe_href?(attr)
+ attribute_name_with_namespace(attr) == 'xlink:href' && !attr.value.start_with?('#')
+ end
+
+ def data_attribute?(attr)
+ attr.name.start_with?('data-') && attr.name =~ DATA_ATTR_PATTERN && attr.namespace.nil?
+ end
end
end
end
diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb
index 917e795b29e..7b6b70d8dbc 100644
--- a/lib/gitlab/sanitizers/svg/whitelist.rb
+++ b/lib/gitlab/sanitizers/svg/whitelist.rb
@@ -4,7 +4,8 @@
module Gitlab
module Sanitizers
module SVG
- ALLOWED_ELEMENTS = %w[
+ class Whitelist
+ ALLOWED_ELEMENTS = %w[
a altGlyph altGlyphDef altGlyphItem animate
animateColor animateMotion animateTransform circle clipPath color-profile
cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
@@ -18,90 +19,91 @@ module Gitlab
script set stop style svg switch symbol text textPath title tref tspan use
view vkern].freeze
- ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
+ ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
- ALLOWED_ATTRIBUTES = {
- 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'altGlyphDef' => %w[id xml:base xml:lang xml:space],
- 'altGlyphItem' => %w[id xml:base xml:lang xml:space],
- 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'desc' => %w[class id style xml:base xml:lang xml:space],
- 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector],
- 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space],
- 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
- 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
- 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
- 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
- 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feMergeNode' => %w[id xml:base xml:lang xml:space],
- 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'fePointLight' => %w[id x xml:base xml:lang xml:space y z],
- 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z],
- 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space],
- 'font-face-format' => %w[id string xml:base xml:lang xml:space],
- 'font-face-name' => %w[id name xml:base xml:lang xml:space],
- 'font-face-src' => %w[id xml:base xml:lang xml:space],
- 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space],
- 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2],
- 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2],
- 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'metadata' => %w[id xml:base xml:lang xml:space],
- 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space],
- 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
- 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'style' => %w[id media title type xml:base xml:lang xml:space],
- 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan],
- 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space],
- 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space],
- 'title' => %w[class id style xml:base xml:lang xml:space],
- 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y],
- 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y],
- 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
- 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan],
- 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space]
- }.freeze
+ ALLOWED_ATTRIBUTES = {
+ 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'altGlyphDef' => %w[id xml:base xml:lang xml:space],
+ 'altGlyphItem' => %w[id xml:base xml:lang xml:space],
+ 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'desc' => %w[class id style xml:base xml:lang xml:space],
+ 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector],
+ 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space],
+ 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
+ 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
+ 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
+ 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space],
+ 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feMergeNode' => %w[id xml:base xml:lang xml:space],
+ 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'fePointLight' => %w[id x xml:base xml:lang xml:space y z],
+ 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z],
+ 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space],
+ 'font-face-format' => %w[id string xml:base xml:lang xml:space],
+ 'font-face-name' => %w[id name xml:base xml:lang xml:space],
+ 'font-face-src' => %w[id xml:base xml:lang xml:space],
+ 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space],
+ 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2],
+ 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2],
+ 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'metadata' => %w[id xml:base xml:lang xml:space],
+ 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space],
+ 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space],
+ 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'style' => %w[id media title type xml:base xml:lang xml:space],
+ 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan],
+ 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space],
+ 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space],
+ 'title' => %w[class id style xml:base xml:lang xml:space],
+ 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y],
+ 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y],
+ 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan],
+ 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space]
+ }.freeze
+ end
end
end
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 2ef0e982256..7cf506ebe64 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -5,7 +5,7 @@ module Gitlab
SeedFu.quiet = true
yield
SeedFu.quiet = false
- puts "\nOK".green
+ puts "\nOK".color(:green)
end
def self.by_user(user)
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index ae85b294d31..104280f520a 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -25,18 +25,18 @@ module Gitlab
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{MAX_RSS}"
- Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} "\
+ Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"\
"in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
- Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}"
+ Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
Process.kill('SIGTERM', Process.pid)
Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
- "#{SHUTDOWN_SIGNAL} to PID #{Process.pid}"
+ "#{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
sleep(SHUTDOWN_WAIT)
- Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid}"
+ Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
Process.kill(SHUTDOWN_SIGNAL, Process.pid)
end
end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
new file mode 100644
index 00000000000..760ff3e614a
--- /dev/null
+++ b/lib/gitlab/template/base_template.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module Template
+ class BaseTemplate
+ def initialize(path)
+ @path = path
+ end
+
+ def name
+ File.basename(@path, self.class.extension)
+ end
+
+ def content
+ File.read(@path)
+ end
+
+ class << self
+ def all
+ self.categories.keys.flat_map { |cat| by_category(cat) }
+ end
+
+ def find(key)
+ file_name = "#{key}#{self.extension}"
+
+ directory = select_directory(file_name)
+ directory ? new(File.join(category_directory(directory), file_name)) : nil
+ end
+
+ def categories
+ raise NotImplementedError
+ end
+
+ def extension
+ raise NotImplementedError
+ end
+
+ def base_dir
+ raise NotImplementedError
+ end
+
+ def by_category(category)
+ templates_for_directory(category_directory(category))
+ end
+
+ def category_directory(category)
+ File.join(base_dir, categories[category])
+ end
+
+ private
+
+ def select_directory(file_name)
+ categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+
+ def templates_for_directory(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
+ end
+
+ def filter_regex
+ @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb
new file mode 100644
index 00000000000..964fbfd4de3
--- /dev/null
+++ b/lib/gitlab/template/gitignore.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Template
+ class Gitignore < BaseTemplate
+ class << self
+ def extension
+ '.gitignore'
+ end
+
+ def categories
+ {
+ "Languages" => '',
+ "Global" => 'Global'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitignore')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb
new file mode 100644
index 00000000000..7f480fe33c0
--- /dev/null
+++ b/lib/gitlab/template/gitlab_ci_yml.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Template
+ class GitlabCiYml < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ '.gitlab-ci.yml'
+ end
+
+ def categories
+ {
+ "General" => '',
+ "Pages" => 'Pages'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitlab-ci-yml')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/timeless.rb b/lib/gitlab/timeless.rb
new file mode 100644
index 00000000000..b290c716f97
--- /dev/null
+++ b/lib/gitlab/timeless.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Timeless
+ def self.timeless(model, &block)
+ original_record_timestamps = model.record_timestamps
+ model.record_timestamps = false
+
+ if block.arity.abs == 1
+ block.call(model)
+ else
+ block.call
+ end
+ ensure
+ model.record_timestamps = original_record_timestamps
+ end
+ end
+end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 2bbbd3074e8..fe65c246101 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -62,7 +62,7 @@ module Gitlab
end
def wiki_page_url
- "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}"
+ namespace_project_wiki_url(object.wiki.project.namespace, object.wiki.project, object.slug)
end
end
end
diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/url_sanitizer.rb
index d23b013c1f5..86ed18fb50d 100644
--- a/lib/gitlab/import_url.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,21 @@
module Gitlab
- class ImportUrl
+ class UrlSanitizer
+ def self.sanitize(content)
+ regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
+
+ content.gsub(regexp) { |url| new(url).masked_url }
+ end
+
+ def self.valid?(url)
+ Addressable::URI.parse(url.strip)
+
+ true
+ rescue Addressable::URI::InvalidURIError
+ false
+ end
+
def initialize(url, credentials: nil)
- @url = URI.parse(URI.encode(url))
+ @url = Addressable::URI.parse(url.strip)
@credentials = credentials
end
@@ -9,6 +23,13 @@ module Gitlab
@sanitized_url ||= safe_url.to_s
end
+ def masked_url
+ url = @url.dup
+ url.password = "*****" unless url.password.nil?
+ url.user = "*****" unless url.user.nil?
+ url.to_s
+ end
+
def credentials
@credentials ||= { user: @url.user, password: @url.password }
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index a1ee1cba216..9462f3368e6 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -32,6 +32,13 @@ module Gitlab
}
end
+ def highest_allowed_level
+ restricted_levels = current_application_settings.restricted_visibility_levels
+
+ allowed_levels = self.values - restricted_levels
+ allowed_levels.max || PRIVATE
+ end
+
def allowed_for?(user, level)
user.is_admin? || allowed_level?(level.to_i)
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c3ddd4c2680..bc0193a6c32 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -6,6 +6,13 @@ module Gitlab
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
class << self
+ def git_http_ok(repository, user)
+ {
+ 'GL_ID' => Gitlab::GlId.gl_id(user),
+ 'RepoPath' => repository.path_to_repo,
+ }
+ end
+
def send_git_blob(repository, blob)
params = {
'RepoPath' => repository.path_to_repo,
@@ -14,24 +21,50 @@ module Gitlab
[
SEND_DATA_HEADER,
- "git-blob:#{encode(params)}",
+ "git-blob:#{encode(params)}"
]
end
- def send_git_archive(project, ref, format)
+ def send_git_archive(repository, ref:, format:)
format ||= 'tar.gz'
format.downcase!
- params = project.repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
+ params = 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)}",
+ "git-archive:#{encode(params)}"
+ ]
+ end
+
+ def send_git_diff(repository, diff_refs)
+ params = {
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => diff_refs.start_sha,
+ 'ShaTo' => diff_refs.head_sha
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "git-diff:#{encode(params)}"
]
end
-
+
+ def send_git_patch(repository, diff_refs)
+ params = {
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => diff_refs.start_sha,
+ 'ShaTo' => diff_refs.head_sha
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "git-format-patch:#{encode(params)}"
+ ]
+ end
+
protected
-
+
def encode(hash)
Base64.urlsafe_encode64(JSON.dump(hash))
end
diff --git a/lib/json_web_token/rsa_token.rb b/lib/json_web_token/rsa_token.rb
new file mode 100644
index 00000000000..d6d6af7089c
--- /dev/null
+++ b/lib/json_web_token/rsa_token.rb
@@ -0,0 +1,42 @@
+module JSONWebToken
+ class RSAToken < Token
+ attr_reader :key_file
+
+ def initialize(key_file)
+ super()
+ @key_file = key_file
+ end
+
+ def encoded
+ headers = {
+ kid: kid
+ }
+ JWT.encode(payload, key, 'RS256', headers)
+ end
+
+ private
+
+ def key_data
+ @key_data ||= File.read(key_file)
+ end
+
+ def key
+ @key ||= OpenSSL::PKey::RSA.new(key_data)
+ end
+
+ def public_key
+ key.public_key
+ end
+
+ def kid
+ # calculate sha256 from DER encoded ASN1
+ kid = Digest::SHA256.digest(public_key.to_der)
+
+ # we encode only 30 bytes with base32
+ kid = Base32.encode(kid[0..29])
+
+ # insert colon every 4 characters
+ kid.scan(/.{4}/).join(':')
+ end
+ end
+end
diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb
new file mode 100644
index 00000000000..5b67715b0b2
--- /dev/null
+++ b/lib/json_web_token/token.rb
@@ -0,0 +1,46 @@
+module JSONWebToken
+ class Token
+ attr_accessor :issuer, :subject, :audience, :id
+ attr_accessor :issued_at, :not_before, :expire_time
+
+ def initialize
+ @id = SecureRandom.uuid
+ @issued_at = Time.now
+ # we give a few seconds for time shift
+ @not_before = issued_at - 5.seconds
+ # default 60 seconds should be more than enough for this authentication token
+ @expire_time = issued_at + 1.minute
+ @custom_payload = {}
+ end
+
+ def [](key)
+ @custom_payload[key]
+ end
+
+ def []=(key, value)
+ @custom_payload[key] = value
+ end
+
+ def encoded
+ raise NotImplementedError
+ end
+
+ def payload
+ @custom_payload.merge(default_payload)
+ end
+
+ private
+
+ def default_payload
+ {
+ jti: id,
+ aud: audience,
+ sub: subject,
+ iss: issuer,
+ iat: issued_at.to_i,
+ nbf: not_before.to_i,
+ exp: expire_time.to_i
+ }.compact
+ end
+ end
+end
diff --git a/lib/support/nginx/registry-ssl b/lib/support/nginx/registry-ssl
new file mode 100644
index 00000000000..92511e26861
--- /dev/null
+++ b/lib/support/nginx/registry-ssl
@@ -0,0 +1,53 @@
+## Lines starting with two hashes (##) are comments with information.
+## Lines starting with one hash (#) are configuration parameters that can be uncommented.
+##
+###################################
+## configuration ##
+###################################
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ listen *:80;
+ server_name registry.gitlab.example.com;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+ return 301 https://$http_host:$request_uri;
+ access_log /var/log/nginx/gitlab_registry_access.log gitlab_access;
+ error_log /var/log/nginx/gitlab_registry_error.log;
+}
+
+server {
+ # If a different port is specified in https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/config/gitlab.yml.example#L182,
+ # it should be declared here as well
+ listen *:443 ssl http2;
+ server_name registry.gitlab.example.com;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ client_max_body_size 0;
+ chunked_transfer_encoding on;
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/gitlab/ssl/registry.gitlab.example.com.crt
+ ssl_certificate_key /etc/gitlab/ssl/registry.gitlab.example.com.key
+
+ ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4';
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache builtin:1000 shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ access_log /var/log/gitlab/nginx/gitlab_registry_access.log gitlab_access;
+ error_log /var/log/gitlab/nginx/gitlab_registry_error.log;
+
+ location / {
+ proxy_set_header Host $http_host; # required for docker client's sake
+ proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 900;
+
+ proxy_pass http://localhost:5000;
+ }
+
+}
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 030ee8bafcb..e930ace1041 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -13,7 +13,7 @@ namespace :gemojione do
aliases[real_name] << alias_name
end
- AwardEmoji.emojis.map do |name, emoji_hash|
+ Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
digest = Digest::SHA256.file(fpath).hexdigest
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 402bb338f27..b43ee5b3383 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -14,6 +14,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
+ Rake::Task["gitlab:backup:registry:create"].invoke
backup = Backup::Manager.new
backup.pack
@@ -32,21 +33,22 @@ namespace :gitlab do
unless backup.skipped?('db')
unless ENV['force'] == 'yes'
- warning = warning = <<-MSG.strip_heredoc
+ warning = <<-MSG.strip_heredoc
Before restoring the database we recommend removing all existing
tables to avoid future upgrade problems. Be aware that if you have
custom tables in the GitLab database these tables and all data will be
removed.
MSG
+ puts warning.color(:red)
ask_to_continue
- puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow
+ puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
sleep(5)
end
# Drop all tables Load the schema to ensure we don't have any newer tables
# hanging out from a failed upgrade
- $progress.puts 'Cleaning the database ... '.blue
+ $progress.puts 'Cleaning the database ... '.color(:blue)
Rake::Task['gitlab:db:drop_tables'].invoke
- $progress.puts 'done'.green
+ $progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke
end
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
@@ -54,6 +56,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
+ Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
backup.cleanup
@@ -61,115 +64,142 @@ namespace :gitlab do
namespace :repo do
task create: :environment do
- $progress.puts "Dumping repositories ...".blue
+ $progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Repository.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring repositories ...".blue
+ $progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
namespace :db do
task create: :environment do
- $progress.puts "Dumping database ... ".blue
+ $progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Database.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring database ... ".blue
+ $progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
namespace :builds do
task create: :environment do
- $progress.puts "Dumping builds ... ".blue
+ $progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Builds.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring builds ... ".blue
+ $progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
namespace :uploads do
task create: :environment do
- $progress.puts "Dumping uploads ... ".blue
+ $progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Uploads.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring uploads ... ".blue
+ $progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
namespace :artifacts do
task create: :environment do
- $progress.puts "Dumping artifacts ... ".blue
+ $progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Artifacts.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring artifacts ... ".blue
+ $progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
namespace :lfs do
task create: :environment do
- $progress.puts "Dumping lfs objects ... ".blue
+ $progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
- $progress.puts "[SKIPPED]".cyan
+ $progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Lfs.new.dump
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
end
end
task restore: :environment do
- $progress.puts "Restoring lfs objects ... ".blue
+ $progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore
- $progress.puts "done".green
+ $progress.puts "done".color(:green)
+ end
+ end
+
+ namespace :registry do
+ task create: :environment do
+ $progress.puts "Dumping container registry images ... ".color(:blue)
+
+ if Gitlab.config.registry.enabled
+ if ENV["SKIP"] && ENV["SKIP"].include?("registry")
+ $progress.puts "[SKIPPED]".color(:cyan)
+ else
+ Backup::Registry.new.dump
+ $progress.puts "done".color(:green)
+ end
+ else
+ $progress.puts "[DISABLED]".color(:cyan)
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring container registry images ... ".color(:blue)
+ if Gitlab.config.registry.enabled
+ Backup::Registry.new.restore
+ $progress.puts "done".color(:green)
+ else
+ $progress.puts "[DISABLED]".color(:cyan)
+ end
end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index effb8eb6001..e9a4e37ec48 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -50,14 +50,14 @@ namespace :gitlab do
end
if correct_options.all?
- puts "yes".green
+ puts "yes".color(:green)
else
print "Trying to fix Git error automatically. ..."
if auto_fix_git_config(options)
- puts "Success".green
+ puts "Success".color(:green)
else
- puts "Failed".red
+ puts "Failed".color(:red)
try_fixing_it(
sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
)
@@ -74,9 +74,9 @@ namespace :gitlab do
database_config_file = Rails.root.join("config", "database.yml")
if File.exists?(database_config_file)
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Copy config/database.yml.<your db> to config/database.yml",
"Check that the information in config/database.yml is correct"
@@ -95,9 +95,9 @@ namespace :gitlab do
gitlab_config_file = Rails.root.join("config", "gitlab.yml")
if File.exists?(gitlab_config_file)
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Copy config/gitlab.yml.example to config/gitlab.yml",
"Update config/gitlab.yml to match your setup"
@@ -114,14 +114,14 @@ namespace :gitlab do
gitlab_config_file = Rails.root.join("config", "gitlab.yml")
unless File.exists?(gitlab_config_file)
- puts "can't check because of previous errors".magenta
+ puts "can't check because of previous errors".color(:magenta)
end
# omniauth or ldap could have been deleted from the file
unless Gitlab.config['git_host']
- puts "no".green
+ puts "no".color(:green)
else
- puts "yes".red
+ puts "yes".color(:red)
try_fixing_it(
"Backup your config/gitlab.yml",
"Copy config/gitlab.yml.example to config/gitlab.yml",
@@ -138,16 +138,16 @@ namespace :gitlab do
print "Init script exists? ... "
if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.magenta
+ puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return
end
script_path = "/etc/init.d/gitlab"
if File.exists?(script_path)
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Install the init script"
)
@@ -162,7 +162,7 @@ namespace :gitlab do
print "Init script up-to-date? ... "
if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.magenta
+ puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return
end
@@ -170,7 +170,7 @@ namespace :gitlab do
script_path = "/etc/init.d/gitlab"
unless File.exists?(script_path)
- puts "can't check because of previous errors".magenta
+ puts "can't check because of previous errors".color(:magenta)
return
end
@@ -178,9 +178,9 @@ namespace :gitlab do
script_content = File.read(script_path)
if recipe_content == script_content
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Redownload the init script"
)
@@ -197,9 +197,9 @@ namespace :gitlab do
migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
unless migration_status =~ /down\s+\d{14}/
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production")
)
@@ -210,13 +210,13 @@ namespace :gitlab do
def check_orphaned_group_members
print "Database contains orphaned GroupMembers? ... "
if GroupMember.where("user_id not in (select id from users)").count > 0
- puts "yes".red
+ puts "yes".color(:red)
try_fixing_it(
"You can delete the orphaned records using something along the lines of:",
sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
)
else
- puts "no".green
+ puts "no".color(:green)
end
end
@@ -226,9 +226,9 @@ namespace :gitlab do
log_path = Rails.root.join("log")
if File.writable?(log_path)
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"sudo chown -R gitlab #{log_path}",
"sudo chmod -R u+rwX #{log_path}"
@@ -246,9 +246,9 @@ namespace :gitlab do
tmp_path = Rails.root.join("tmp")
if File.writable?(tmp_path)
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"sudo chown -R gitlab #{tmp_path}",
"sudo chmod -R u+rwX #{tmp_path}"
@@ -264,7 +264,7 @@ namespace :gitlab do
print "Uploads directory setup correctly? ... "
unless File.directory?(Rails.root.join('public/uploads'))
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
)
@@ -280,16 +280,16 @@ namespace :gitlab do
if File.stat(upload_path).mode == 040700
unless Dir.exists?(upload_path_tmp)
- puts 'skipped (no tmp uploads folder yet)'.magenta
+ puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
return
end
# 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
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"sudo chown -R #{gitlab_user} #{upload_path}",
"sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
@@ -301,9 +301,9 @@ namespace :gitlab do
fix_and_rerun
end
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
- "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
+ "sudo chmod 700 #{upload_path}"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -320,9 +320,9 @@ namespace :gitlab do
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Update your redis server to a version >= #{min_redis_version}"
)
@@ -356,97 +356,108 @@ namespace :gitlab do
########################
def check_repo_base_exists
- print "Repo base directory exists? ... "
+ puts "Repo base directory exists?"
- repo_base_path = Gitlab.config.gitlab_shell.repos_path
+ Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ print "#{name}... "
- if File.exists?(repo_base_path)
- puts "yes".green
- else
- puts "no".red
- puts "#{repo_base_path} is missing".red
- try_fixing_it(
- "This should have been created when setting up GitLab Shell.",
- "Make sure it's set correctly in config/gitlab.yml",
- "Make sure GitLab Shell is installed correctly."
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
+ if File.exists?(repo_base_path)
+ puts "yes".color(:green)
+ else
+ puts "no".color(:red)
+ puts "#{repo_base_path} is missing".color(:red)
+ try_fixing_it(
+ "This should have been created when setting up GitLab Shell.",
+ "Make sure it's set correctly in config/gitlab.yml",
+ "Make sure GitLab Shell is installed correctly."
+ )
+ for_more_information(
+ see_installation_guide_section "GitLab Shell"
+ )
+ fix_and_rerun
+ end
end
end
def check_repo_base_is_not_symlink
- print "Repo base directory is a symlink? ... "
+ puts "Repo storage directories are symlinks?"
- repo_base_path = Gitlab.config.gitlab_shell.repos_path
- unless File.exists?(repo_base_path)
- puts "can't check because of previous errors".magenta
- return
- end
+ Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ print "#{name}... "
- unless File.symlink?(repo_base_path)
- puts "no".green
- else
- puts "yes".red
- try_fixing_it(
- "Make sure it's set to the real directory in config/gitlab.yml"
- )
- fix_and_rerun
+ unless File.exists?(repo_base_path)
+ puts "can't check because of previous errors".color(:magenta)
+ return
+ end
+
+ unless File.symlink?(repo_base_path)
+ puts "no".color(:green)
+ else
+ puts "yes".color(:red)
+ try_fixing_it(
+ "Make sure it's set to the real directory in config/gitlab.yml"
+ )
+ fix_and_rerun
+ end
end
end
def check_repo_base_permissions
- print "Repo base access is drwxrws---? ... "
+ puts "Repo paths access is drwxrws---?"
- repo_base_path = Gitlab.config.gitlab_shell.repos_path
- unless File.exists?(repo_base_path)
- puts "can't check because of previous errors".magenta
- return
- end
+ Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ print "#{name}... "
- if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
- puts "yes".green
- else
- puts "no".red
- try_fixing_it(
- "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
- "sudo chmod -R ug-s #{repo_base_path}",
- "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
+ unless File.exists?(repo_base_path)
+ puts "can't check because of previous errors".color(:magenta)
+ return
+ end
+
+ if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
+ puts "yes".color(:green)
+ else
+ puts "no".color(:red)
+ try_fixing_it(
+ "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
+ "sudo chmod -R ug-s #{repo_base_path}",
+ "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
+ )
+ for_more_information(
+ see_installation_guide_section "GitLab Shell"
+ )
+ fix_and_rerun
+ end
end
end
def check_repo_base_user_and_group
gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user
gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
- print "Repo base owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}? ... "
+ puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
- repo_base_path = Gitlab.config.gitlab_shell.repos_path
- unless File.exists?(repo_base_path)
- puts "can't check because of previous errors".magenta
- return
- end
+ Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ print "#{name}... "
- uid = uid_for(gitlab_shell_ssh_user)
- gid = gid_for(gitlab_shell_owner_group)
- if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid
- puts "yes".green
- else
- puts "no".red
- puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue
- try_fixing_it(
- "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab Shell"
- )
- fix_and_rerun
+ unless File.exists?(repo_base_path)
+ puts "can't check because of previous errors".color(:magenta)
+ return
+ end
+
+ uid = uid_for(gitlab_shell_ssh_user)
+ gid = gid_for(gitlab_shell_owner_group)
+ if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid
+ puts "yes".color(:green)
+ else
+ puts "no".color(:red)
+ puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue)
+ try_fixing_it(
+ "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}"
+ )
+ for_more_information(
+ see_installation_guide_section "GitLab Shell"
+ )
+ fix_and_rerun
+ end
end
end
@@ -456,7 +467,7 @@ namespace :gitlab do
gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path
unless Project.count > 0
- puts "can't check, you have no projects".magenta
+ puts "can't check, you have no projects".color(:magenta)
return
end
puts ""
@@ -466,14 +477,14 @@ namespace :gitlab do
project_hook_directory = File.join(project.repository.path_to_repo, "hooks")
if project.empty_repo?
- puts "repository is empty".magenta
+ puts "repository is empty".color(:magenta)
elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) &&
(File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path))
- puts 'ok'.green
+ puts 'ok'.color(:green)
else
- puts "wrong or missing hooks".red
+ puts "wrong or missing hooks".color(:red)
try_fixing_it(
- sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"),
+ sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')} #{repository_storage_paths_args.join(' ')}"),
'Check the hooks_path in config/gitlab.yml',
'Check your gitlab-shell installation'
)
@@ -491,9 +502,9 @@ namespace :gitlab do
check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base)
puts "Running #{check_cmd}"
if system(check_cmd, chdir: gitlab_shell_repo_base)
- puts 'gitlab-shell self-check successful'.green
+ puts 'gitlab-shell self-check successful'.color(:green)
else
- puts 'gitlab-shell self-check failed'.red
+ puts 'gitlab-shell self-check failed'.color(:red)
try_fixing_it(
'Make sure GitLab is running;',
'Check the gitlab-shell configuration file:',
@@ -507,7 +518,7 @@ namespace :gitlab do
print "projects have namespace: ... "
unless Project.count > 0
- puts "can't check, you have no projects".magenta
+ puts "can't check, you have no projects".color(:magenta)
return
end
puts ""
@@ -516,9 +527,9 @@ namespace :gitlab do
print sanitized_message(project)
if project.namespace
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Migrate global projects"
)
@@ -576,9 +587,9 @@ namespace :gitlab do
print "Running? ... "
if sidekiq_process_count > 0
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
sudo_gitlab("RAILS_ENV=production bin/background_jobs start")
)
@@ -596,9 +607,9 @@ namespace :gitlab do
print 'Number of Sidekiq processes ... '
if process_count == 1
- puts '1'.green
+ puts '1'.color(:green)
else
- puts "#{process_count}".red
+ puts "#{process_count}".color(:red)
try_fixing_it(
'sudo service gitlab stop',
"sudo pkill -u #{gitlab_user} -f sidekiq",
@@ -646,16 +657,16 @@ namespace :gitlab do
print "Init.d configured correctly? ... "
if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.magenta
+ puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return
end
path = "/etc/default/gitlab"
if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Enable mail_room in the init.d configuration."
)
@@ -672,9 +683,9 @@ namespace :gitlab do
path = Rails.root.join("Procfile")
if File.exist?(path) && File.read(path) =~ /^mail_room:/
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Enable mail_room in your Procfile."
)
@@ -691,14 +702,14 @@ namespace :gitlab do
path = "/etc/default/gitlab"
unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "can't check because of previous errors".magenta
+ puts "can't check because of previous errors".color(:magenta)
return
end
if mail_room_running?
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
sudo_gitlab("RAILS_ENV=production bin/mail_room start")
)
@@ -729,9 +740,9 @@ namespace :gitlab do
end
if connected
- puts "yes".green
+ puts "yes".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Check that the information in config/gitlab.yml is correct"
)
@@ -785,13 +796,13 @@ namespace :gitlab do
namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
- namespace_dirs = Dir.glob(
- File.join(Gitlab.config.gitlab_shell.repos_path, '*')
- )
+ Gitlab.config.repositories.storages.each do |name, path|
+ namespace_dirs = Dir.glob(File.join(path, '*'))
- namespace_dirs.each do |namespace_dir|
- repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
- repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
+ namespace_dirs.each do |namespace_dir|
+ repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
+ repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
+ end
end
end
end
@@ -799,19 +810,19 @@ namespace :gitlab do
namespace :user do
desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args|
- username = args[:username] || prompt("Check repository integrity for which username? ".blue)
+ username = args[:username] || prompt("Check repository integrity for fsername? ".color(:blue))
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
File.join(
- Gitlab.config.gitlab_shell.repos_path,
+ p.repository_storage_path,
"#{p.path_with_namespace}.git"
)
end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
else
- puts "\nUser '#{username}' not found".red
+ puts "\nUser '#{username}' not found".color(:red)
end
end
end
@@ -820,13 +831,13 @@ namespace :gitlab do
##########################
def fix_and_rerun
- puts " Please #{"fix the error above"} and rerun the checks.".red
+ puts " Please #{"fix the error above"} and rerun the checks.".color(:red)
end
def for_more_information(*sources)
sources = sources.shift if sources.first.is_a?(Array)
- puts " For more information see:".blue
+ puts " For more information see:".color(:blue)
sources.each do |source|
puts " #{source}"
end
@@ -834,7 +845,7 @@ namespace :gitlab do
def finished_checking(component)
puts ""
- puts "Checking #{component.yellow} ... #{"Finished".green}"
+ puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}"
puts ""
end
@@ -855,14 +866,14 @@ namespace :gitlab do
end
def start_checking(component)
- puts "Checking #{component.yellow} ..."
+ puts "Checking #{component.color(:yellow)} ..."
puts ""
end
def try_fixing_it(*steps)
steps = steps.shift if steps.first.is_a?(Array)
- puts " Try fixing it:".blue
+ puts " Try fixing it:".color(:blue)
steps.each do |step|
puts " #{step}"
end
@@ -874,9 +885,9 @@ namespace :gitlab do
print "GitLab Shell version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version
- puts "OK (#{current_version})".green
+ puts "OK (#{current_version})".color(:green)
else
- puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red
+ puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
end
end
@@ -887,9 +898,9 @@ namespace :gitlab do
print "Ruby version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".green
+ puts "yes (#{current_version})".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Update your ruby to a version >= #{required_version} from #{current_version}"
)
@@ -905,9 +916,9 @@ namespace :gitlab do
print "Git version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".green
+ puts "yes (#{current_version})".color(:green)
else
- puts "no".red
+ puts "no".color(:red)
try_fixing_it(
"Update your git to a version >= #{required_version} from #{current_version}"
)
@@ -925,9 +936,9 @@ namespace :gitlab do
def sanitized_message(project)
if should_sanitize?
- "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... "
+ "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else
- "#{project.name_with_namespace.yellow} ... "
+ "#{project.name_with_namespace.color(:yellow)} ... "
end
end
@@ -940,7 +951,7 @@ namespace :gitlab do
end
def check_repo_integrity(repo_dir)
- puts "\nChecking repo at #{repo_dir.yellow}"
+ puts "\nChecking repo at #{repo_dir.color(:yellow)}"
git_fsck(repo_dir)
check_config_lock(repo_dir)
@@ -948,25 +959,25 @@ namespace :gitlab do
end
def git_fsck(repo_dir)
- puts "Running `git fsck`".yellow
+ puts "Running `git fsck`".color(:yellow)
system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir)
end
def check_config_lock(repo_dir)
config_exists = File.exist?(File.join(repo_dir,'config.lock'))
- config_output = config_exists ? 'yes'.red : 'no'.green
- puts "'config.lock' file exists?".yellow + " ... #{config_output}"
+ config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
+ puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
if lock_files.present?
- puts "Ref lock files exist:".red
+ puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file|
puts " #{lock_file}"
end
else
- puts "No ref lock files exist".green
+ puts "No ref lock files exist".color(:green)
end
end
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 9f5852ac613..b7cbdc6cd78 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -5,41 +5,41 @@ namespace :gitlab do
warn_user_is_not_gitlab
remove_flag = ENV['REMOVE']
-
namespaces = Namespace.pluck(:path)
- git_base_path = Gitlab.config.gitlab_shell.repos_path
- all_dirs = Dir.glob(git_base_path + '/*')
+ Gitlab.config.repositories.storages.each do |name, git_base_path|
+ all_dirs = Dir.glob(git_base_path + '/*')
- puts git_base_path.yellow
- puts "Looking for directories to remove... "
+ puts git_base_path.color(:yellow)
+ puts "Looking for directories to remove... "
- all_dirs.reject! do |dir|
- # skip if git repo
- dir =~ /.git$/
- end
+ all_dirs.reject! do |dir|
+ # skip if git repo
+ dir =~ /.git$/
+ end
- all_dirs.reject! do |dir|
- dir_name = File.basename dir
+ all_dirs.reject! do |dir|
+ dir_name = File.basename dir
- # skip if namespace present
- namespaces.include?(dir_name)
- end
+ # skip if namespace present
+ namespaces.include?(dir_name)
+ end
- all_dirs.each do |dir_path|
+ all_dirs.each do |dir_path|
- if remove_flag
- if FileUtils.rm_rf dir_path
- puts "Removed...#{dir_path}".red
+ if remove_flag
+ if FileUtils.rm_rf dir_path
+ puts "Removed...#{dir_path}".color(:red)
+ else
+ puts "Cannot remove #{dir_path}".color(:red)
+ end
else
- puts "Cannot remove #{dir_path}".red
+ puts "Can be removed: #{dir_path}".color(:red)
end
- else
- puts "Can be removed: #{dir_path}".red
end
end
unless remove_flag
- puts "To cleanup this directories run this command with REMOVE=true".yellow
+ puts "To cleanup this directories run this command with REMOVE=true".color(:yellow)
end
end
@@ -48,20 +48,21 @@ namespace :gitlab do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
- repo_root = Gitlab.config.gitlab_shell.repos_path
- # Look for global repos (legacy, depth 1) and normal repos (depth 2)
- IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
- find.each_line do |path|
- path.chomp!
- repo_with_namespace = path.
- sub(repo_root, '').
- sub(%r{^/*}, '').
- chomp('.git').
- chomp('.wiki')
- next if Project.find_with_namespace(repo_with_namespace)
- new_path = path + move_suffix
- puts path.inspect + ' -> ' + new_path.inspect
- File.rename(path, new_path)
+ Gitlab.config.repositories.storages.each do |name, repo_root|
+ # Look for global repos (legacy, depth 1) and normal repos (depth 2)
+ IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
+ find.each_line do |path|
+ path.chomp!
+ repo_with_namespace = path.
+ sub(repo_root, '').
+ sub(%r{^/*}, '').
+ chomp('.git').
+ chomp('.wiki')
+ next if Project.find_with_namespace(repo_with_namespace)
+ new_path = path + move_suffix
+ puts path.inspect + ' -> ' + new_path.inspect
+ File.rename(path, new_path)
+ end
end
end
end
@@ -75,19 +76,19 @@ namespace :gitlab do
next unless user.ldap_user?
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
if Gitlab::LDAP::Access.allowed?(user)
- puts " [OK]".green
+ puts " [OK]".color(:green)
else
if block_flag
user.block! unless user.blocked?
- puts " [BLOCKED]".red
+ puts " [BLOCKED]".color(:red)
else
- puts " [NOT IN LDAP]".yellow
+ puts " [NOT IN LDAP]".color(:yellow)
end
end
end
unless block_flag
- puts "To block these users run this command with BLOCK=true".yellow
+ puts "To block these users run this command with BLOCK=true".color(:yellow)
end
end
end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index e473b756023..7230b9485be 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -3,22 +3,22 @@ namespace :gitlab do
desc 'GitLab | Manually insert schema migration version'
task :mark_migration_complete, [:version] => :environment do |_, args|
unless args[:version]
- puts "Must specify a migration version as an argument".red
+ puts "Must specify a migration version as an argument".color(:red)
exit 1
end
version = args[:version].to_i
if version == 0
- puts "Version '#{args[:version]}' must be a non-zero integer".red
+ puts "Version '#{args[:version]}' must be a non-zero integer".color(:red)
exit 1
end
sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
begin
ActiveRecord::Base.connection.execute(sql)
- puts "Successfully marked '#{version}' as complete".green
+ puts "Successfully marked '#{version}' as complete".color(:green)
rescue ActiveRecord::RecordNotUnique
- puts "Migration version '#{version}' is already marked complete".yellow
+ puts "Migration version '#{version}' is already marked complete".color(:yellow)
end
end
@@ -34,7 +34,17 @@ namespace :gitlab do
# PG: http://www.postgresql.org/docs/current/static/ddl-depend.html
# MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html
# Add `IF EXISTS` because cascade could have already deleted a table.
- tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{t} CASCADE") }
+ tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") }
+ end
+
+ desc 'Configures the database by running migrate, or by loading the schema and seeding if needed'
+ task configure: :environment do
+ if ActiveRecord::Base.connection.tables.any?
+ Rake::Task['db:migrate'].invoke
+ else
+ Rake::Task['db:schema:load'].invoke
+ Rake::Task['db:seed_fu'].invoke
+ end
end
end
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index 65ee430d550..f9834a4dae8 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -5,7 +5,7 @@ namespace :gitlab do
task repack: :environment do
failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
if failures.empty?
- puts "Done".green
+ puts "Done".color(:green)
else
output_failures(failures)
end
@@ -15,7 +15,7 @@ namespace :gitlab do
task gc: :environment do
failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
if failures.empty?
- puts "Done".green
+ puts "Done".color(:green)
else
output_failures(failures)
end
@@ -25,7 +25,7 @@ namespace :gitlab do
task prune: :environment do
failures = perform_git_cmd(%W(git prune), "Git Prune")
if failures.empty?
- puts "Done".green
+ puts "Done".color(:green)
else
output_failures(failures)
end
@@ -47,7 +47,7 @@ namespace :gitlab do
end
def output_failures(failures)
- puts "The following repositories reported errors:".red
+ puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index 1c04f47f08f..dbdd4e977e8 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -2,78 +2,78 @@ namespace :gitlab do
namespace :import do
# How to use:
#
- # 1. copy the bare repos under the repos_path (commonly /home/git/repositories)
+ # 1. copy the bare repos under the repository storage paths (commonly the default path is /home/git/repositories)
# 2. run: bundle exec rake gitlab:import:repos RAILS_ENV=production
#
# Notes:
# * The project owner will set to the first administator of the system
# * Existing projects will be skipped
#
- desc "GitLab | Import bare repositories from gitlab_shell -> repos_path into GitLab project instance"
+ desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
task repos: :environment do
+ Gitlab.config.repositories.storages.each do |name, git_base_path|
+ repos_to_import = Dir.glob(git_base_path + '/**/*.git')
- git_base_path = Gitlab.config.gitlab_shell.repos_path
- repos_to_import = Dir.glob(git_base_path + '/**/*.git')
+ repos_to_import.each do |repo_path|
+ # strip repo base path
+ repo_path[0..git_base_path.length] = ''
- repos_to_import.each do |repo_path|
- # strip repo base path
- repo_path[0..git_base_path.length] = ''
+ path = repo_path.sub(/\.git$/, '')
+ group_name, name = File.split(path)
+ group_name = nil if group_name == '.'
- path = repo_path.sub(/\.git$/, '')
- group_name, name = File.split(path)
- group_name = nil if group_name == '.'
+ puts "Processing #{repo_path}".color(:yellow)
- puts "Processing #{repo_path}".yellow
-
- if path.end_with?('.wiki')
- puts " * Skipping wiki repo"
- next
- end
+ if path.end_with?('.wiki')
+ puts " * Skipping wiki repo"
+ next
+ end
- project = Project.find_with_namespace(path)
+ project = Project.find_with_namespace(path)
- if project
- puts " * #{project.name} (#{repo_path}) exists"
- else
- user = User.admins.reorder("id").first
+ if project
+ puts " * #{project.name} (#{repo_path}) exists"
+ else
+ user = User.admins.reorder("id").first
- project_params = {
- name: name,
- path: name
- }
+ project_params = {
+ name: name,
+ path: name
+ }
- # find group namespace
- if group_name
- group = Namespace.find_by(path: group_name)
- # create group namespace
- unless group
- group = Group.new(:name => group_name)
- group.path = group_name
- group.owner = user
- if group.save
- puts " * Created Group #{group.name} (#{group.id})".green
- else
- puts " * Failed trying to create group #{group.name}".red
+ # find group namespace
+ if group_name
+ group = Namespace.find_by(path: group_name)
+ # create group namespace
+ unless group
+ group = Group.new(:name => group_name)
+ group.path = group_name
+ group.owner = user
+ if group.save
+ puts " * Created Group #{group.name} (#{group.id})".color(:green)
+ else
+ puts " * Failed trying to create group #{group.name}".color(:red)
+ end
end
+ # set project group
+ project_params[:namespace_id] = group.id
end
- # set project group
- project_params[:namespace_id] = group.id
- end
- project = Projects::CreateService.new(user, project_params).execute
+ project = Projects::CreateService.new(user, project_params).execute
- if project.persisted?
- puts " * Created #{project.name} (#{repo_path})".green
- project.update_repository_size
- project.update_commit_count
- else
- puts " * Failed trying to create #{project.name} (#{repo_path})".red
- puts " Errors: #{project.errors.messages}".red
+ if project.persisted?
+ puts " * Created #{project.name} (#{repo_path})".color(:green)
+ project.update_repository_size
+ project.update_commit_count
+ else
+ puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
+ puts " Errors: #{project.errors.messages}".color(:red)
+ end
end
end
end
- puts "Done!".green
+ puts "Done!".color(:green)
end
end
end
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
new file mode 100644
index 00000000000..c2c6031db67
--- /dev/null
+++ b/lib/tasks/gitlab/import_export.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :import_export do
+ desc "GitLab | Show Import/Export version"
+ task version: :environment do
+ puts "Import/Export v#{Gitlab::ImportExport.version}"
+ end
+
+ desc "GitLab | Display exported DB structure"
+ task data: :environment do
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index d6883a563ee..fe43d40e6d2 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -15,15 +15,15 @@ namespace :gitlab do
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
puts ""
- puts "System information".yellow
- puts "System:\t\t#{os_name || "unknown".red}"
+ puts "System information".color(:yellow)
+ puts "System:\t\t#{os_name || "unknown".color(:red)}"
puts "Current User:\t#{run(%W(whoami))}"
- puts "Using RVM:\t#{rvm_version.present? ? "yes".green : "no"}"
+ puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
- puts "Ruby Version:\t#{ruby_version || "unknown".red}"
- puts "Gem Version:\t#{gem_version || "unknown".red}"
- puts "Bundler Version:#{bunder_version || "unknown".red}"
- puts "Rake Version:\t#{rake_version || "unknown".red}"
+ puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
+ puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
+ puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
+ puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
@@ -39,7 +39,7 @@ namespace :gitlab do
omniauth_providers.map! { |provider| provider['name'] }
puts ""
- puts "GitLab information".yellow
+ puts "GitLab information".color(:yellow)
puts "Version:\t#{Gitlab::VERSION}"
puts "Revision:\t#{Gitlab::REVISION}"
puts "Directory:\t#{Rails.root}"
@@ -47,9 +47,9 @@ namespace :gitlab do
puts "URL:\t\t#{Gitlab.config.gitlab.url}"
puts "HTTP Clone URL:\t#{http_clone_url}"
puts "SSH Clone URL:\t#{ssh_clone_url}"
- puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}"
- puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}"
- puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled
+ puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}"
+ puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
+ puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
@@ -60,9 +60,12 @@ namespace :gitlab do
end
puts ""
- puts "GitLab Shell".yellow
- puts "Version:\t#{gitlab_shell_version || "unknown".red}"
- puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}"
+ puts "GitLab Shell".color(:yellow)
+ puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
+ puts "Repository storage paths:"
+ Gitlab.config.repositories.storages.each do |name, path|
+ puts "- #{name}: \t#{path}"
+ end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake
index c7596e7abcb..ffcc76e5498 100644
--- a/lib/tasks/gitlab/list_repos.rake
+++ b/lib/tasks/gitlab/list_repos.rake
@@ -9,7 +9,7 @@ namespace :gitlab do
scope = scope.where('id IN (?) OR namespace_id in (?)', project_ids, namespace_ids)
end
scope.find_each do |project|
- base = File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace)
+ base = File.join(project.repository_storage_path, project.path_with_namespace)
puts base + '.git'
puts base + '.wiki.git'
end
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 48baecfd2a2..05fcb8e3da5 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -19,7 +19,7 @@ namespace :gitlab do
Rake::Task["setup_postgresql"].invoke
Rake::Task["db:seed_fu"].invoke
rescue Gitlab::TaskAbortedByUserError
- puts "Quitting...".red
+ puts "Quitting...".color(:red)
exit 1
end
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index dd61632e557..c85ebdf8619 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -12,7 +12,6 @@ namespace :gitlab do
gitlab_url = Gitlab.config.gitlab.url
# gitlab-shell requires a / at the end of the url
gitlab_url += '/' unless gitlab_url.end_with?('/')
- repos_path = Gitlab.config.gitlab_shell.repos_path
target_dir = Gitlab.config.gitlab_shell.path
# Clone if needed
@@ -35,7 +34,6 @@ namespace :gitlab do
user: user,
gitlab_url: gitlab_url,
http_settings: {self_signed_cert: false}.stringify_keys,
- repos_path: repos_path,
auth_file: File.join(home_dir, ".ssh", "authorized_keys"),
redis: {
bin: %x{which redis-cli}.chomp,
@@ -58,10 +56,10 @@ namespace :gitlab do
File.open("config.yml", "w+") {|f| f.puts config.to_yaml}
# Launch installation process
- system(*%W(bin/install))
+ system(*%W(bin/install) + repository_storage_paths_args)
# (Re)create hooks
- system(*%W(bin/create-hooks))
+ system(*%W(bin/create-hooks) + repository_storage_paths_args)
end
# Required for debian packaging with PKGR: Setup .ssh/environment with
@@ -73,6 +71,8 @@ namespace :gitlab do
File.open(File.join(home_dir, ".ssh", "environment"), "w+") do |f|
f.puts "PATH=#{ENV['PATH']}"
end
+
+ Gitlab::Shell.new.generate_and_link_secret_token
end
desc "GitLab | Setup gitlab-shell"
@@ -87,7 +87,8 @@ namespace :gitlab do
if File.exists?(path_to_repo)
print '-'
else
- if Gitlab::Shell.new.add_repository(project.path_with_namespace)
+ if Gitlab::Shell.new.add_repository(project.repository_storage_path,
+ project.path_with_namespace)
print '.'
else
print 'F'
@@ -118,12 +119,12 @@ namespace :gitlab do
puts ""
unless $?.success?
- puts "Failed to add keys...".red
+ puts "Failed to add keys...".color(:red)
exit 1
end
rescue Gitlab::TaskAbortedByUserError
- puts "Quitting...".red
+ puts "Quitting...".color(:red)
exit 1
end
@@ -138,4 +139,3 @@ namespace :gitlab do
system(*%W(#{Gitlab.config.git.bin_path} reset --hard #{tag}))
end
end
-
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index d33b5b31e18..ab96b1d3593 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -2,7 +2,7 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
-String.disable_colorization = true unless STDOUT.isatty
+require 'rainbow/ext/string'
# Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
@@ -14,7 +14,7 @@ namespace :gitlab do
# Returns "yes" the user chose to continue
# Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
def ask_to_continue
- answer = prompt("Do you want to continue (yes/no)? ".blue, %w{yes no})
+ answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
raise Gitlab::TaskAbortedByUserError unless answer == "yes"
end
@@ -98,10 +98,10 @@ namespace :gitlab do
gitlab_user = Gitlab.config.gitlab.user
current_user = run(%W(whoami)).chomp
unless current_user == gitlab_user
- puts " Warning ".colorize(:black).on_yellow
- puts " You are running as user #{current_user.magenta}, we hope you know what you are doing."
+ puts " Warning ".color(:black).background(:yellow)
+ puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
puts " Things may work\/fail for the wrong reasons."
- puts " For correct results you should run this as user #{gitlab_user.magenta}."
+ puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
puts ""
end
@warned_user_not_gitlab = true
@@ -125,10 +125,16 @@ namespace :gitlab do
end
def all_repos
- IO.popen(%W(find #{Gitlab.config.gitlab_shell.repos_path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
- find.each_line do |path|
- yield path.chomp
+ Gitlab.config.repositories.storages.each do |name, path|
+ IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ find.each_line do |path|
+ yield path.chomp
+ end
end
end
end
+
+ def repository_storage_paths_args
+ Gitlab.config.repositories.storages.values
+ end
end
diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake
index 9196677a017..fc0ccc726ed 100644
--- a/lib/tasks/gitlab/two_factor.rake
+++ b/lib/tasks/gitlab/two_factor.rake
@@ -6,17 +6,17 @@ namespace :gitlab do
count = scope.count
if count > 0
- puts "This will disable 2FA for #{count.to_s.red} users..."
+ puts "This will disable 2FA for #{count.to_s.color(:red)} users..."
begin
ask_to_continue
scope.find_each(&:disable_two_factor!)
- puts "Successfully disabled 2FA for #{count} users.".green
+ puts "Successfully disabled 2FA for #{count} users.".color(:green)
rescue Gitlab::TaskAbortedByUserError
- puts "Quitting...".red
+ puts "Quitting...".color(:red)
end
else
- puts "There are currently no users with 2FA enabled.".yellow
+ puts "There are currently no users with 2FA enabled.".color(:yellow)
end
end
end
diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake
index 9b636f12d9f..3bd10b0208b 100644
--- a/lib/tasks/gitlab/update_commit_count.rake
+++ b/lib/tasks/gitlab/update_commit_count.rake
@@ -6,15 +6,15 @@ namespace :gitlab do
ask_to_continue unless ENV['force'] == 'yes'
projects.find_each(batch_size: 100) do |project|
- print "#{project.name_with_namespace.yellow} ... "
+ print "#{project.name_with_namespace.color(:yellow)} ... "
unless project.repo_exists?
- puts "skipping, because the repo is empty".magenta
+ puts "skipping, because the repo is empty".color(:magenta)
next
end
project.update_commit_count
- puts project.commit_count.to_s.green
+ puts project.commit_count.to_s.color(:green)
end
end
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
new file mode 100644
index 00000000000..4f76dad7286
--- /dev/null
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -0,0 +1,54 @@
+namespace :gitlab do
+ desc "GitLab | Update templates"
+ task :update_templates do
+ TEMPLATE_DATA.each { |template| update(template) }
+ end
+
+ def update(template)
+ sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1]
+ dir = File.join(vendor_directory, sub_dir)
+
+ unless clone_repository(template.repo_url, dir)
+ puts "Cloning the #{sub_dir} templates failed".red
+ return
+ end
+
+ remove_unneeded_files(dir, template.cleanup_regex)
+ puts "Done".green
+ end
+
+ def clone_repository(url, directory)
+ FileUtils.rm_rf(directory) if Dir.exist?(directory)
+
+ system("git clone #{url} --depth=1 --branch=master #{directory}")
+ end
+
+ # Retain only certain files:
+ # - The LICENSE, because we have to
+ # - The sub dirs so we can organise the file by category
+ # - The templates themself
+ # - Dir.entries returns also the entries '.' and '..'
+ def remove_unneeded_files(directory, regex)
+ Dir.foreach(directory) do |file|
+ FileUtils.rm_rf(File.join(directory, file)) unless file =~ regex
+ end
+ end
+
+ private
+
+ Template = Struct.new(:repo_url, :cleanup_regex)
+ TEMPLATE_DATA = [
+ Template.new(
+ "https://github.com/github/gitignore.git",
+ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/
+ ),
+ Template.new(
+ "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
+ /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ )
+ ]
+
+ def vendor_directory
+ Rails.root.join('vendor')
+ end
+end
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index cc0f668474e..f467cc0ee29 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -12,9 +12,9 @@ namespace :gitlab do
print "- #{project.name} ... "
web_hook = project.hooks.new(url: web_hook_url)
if web_hook.save
- puts "added".green
+ puts "added".color(:green)
else
- print "failed".red
+ print "failed".color(:red)
puts " [#{web_hook.errors.full_messages.to_sentence}]"
end
end
@@ -57,7 +57,7 @@ namespace :gitlab do
if namespace
Project.in_namespace(namespace.id)
else
- puts "Namespace not found: #{namespace_path}".red
+ puts "Namespace not found: #{namespace_path}".color(:red)
exit 2
end
end
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index d258c6fd08d..4f2486157b7 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -1,6 +1,6 @@
desc "GitLab | Build internal ids for issues and merge requests"
task migrate_iids: :environment do
- puts 'Issues'.yellow
+ puts 'Issues'.color(:yellow)
Issue.where(iid: nil).find_each(batch_size: 100) do |issue|
begin
issue.set_iid
@@ -15,7 +15,7 @@ task migrate_iids: :environment do
end
puts 'done'
- puts 'Merge Requests'.yellow
+ puts 'Merge Requests'.color(:yellow)
MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr|
begin
mr.set_iid
@@ -30,7 +30,7 @@ task migrate_iids: :environment do
end
puts 'done'
- puts 'Milestones'.yellow
+ puts 'Milestones'.color(:yellow)
Milestone.where(iid: nil).find_each(batch_size: 100) do |m|
begin
m.set_iid
diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake
index ddfaf5d51f2..78ffccc9d06 100644
--- a/lib/tasks/rubocop.rake
+++ b/lib/tasks/rubocop.rake
@@ -1,4 +1,5 @@
unless Rails.env.production?
require 'rubocop/rake_task'
+
RuboCop::RakeTask.new
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 01d23b89bb7..da255f5464b 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -52,7 +52,7 @@ def run_spinach_tests(tags)
tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
puts ''
- puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red
+ puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red)
puts ''
sleep(3)
success = run_spinach_command(tests)
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index c5666d49e61..21c0e5f1d41 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -6,8 +6,6 @@ task :test do
end
unless Rails.env.production?
- require 'coveralls/rake/task'
- Coveralls::RakeTask.new
desc "GitLab | Run all tests on CI with simplecov"
- task :test_ci => [:rubocop, :brakeman, 'teaspoon', :spinach, :spec, 'coveralls:push']
+ task test_ci: [:rubocop, :brakeman, 'teaspoon', :spinach, :spec]
end
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index d4291f012d3..41dee5fdc06 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -3,7 +3,6 @@ require "fileutils"
# Taken from: Rack::Test::UploadedFile
class UploadedFile
-
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png
index 7da5f23ed9b..05c8b0d0ccf 100644
--- a/public/apple-touch-icon-precomposed.png
+++ b/public/apple-touch-icon-precomposed.png
Binary files differ
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
index 7da5f23ed9b..05c8b0d0ccf 100644
--- a/public/apple-touch-icon.png
+++ b/public/apple-touch-icon.png
Binary files differ
diff --git a/public/robots.txt b/public/robots.txt
index 4f616c7f4c1..334f4c03533 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -65,3 +65,4 @@ Disallow: /*/*/deploy_keys
Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
+Disallow: /*/*/uploads/
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
new file mode 100644
index 00000000000..d9247a1f7ea
--- /dev/null
+++ b/rubocop/cop/migration/add_index.rb
@@ -0,0 +1,46 @@
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if indexes are added in a concurrent manner.
+ class AddIndex < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'add_index requires downtime, use add_concurrent_index instead'
+
+ def on_def(node)
+ return unless in_migration?(node)
+
+ new_tables = []
+
+ node.each_descendant(:send) do |send_node|
+ first_arg = first_argument(send_node)
+
+ # The first argument of "create_table" / "add_index" is the table
+ # name.
+ new_tables << first_arg if create_table?(send_node)
+
+ next if method_name(send_node) != :add_index
+
+ # Using "add_index" is fine for newly created tables as there's no
+ # data in these tables yet.
+ next if new_tables.include?(first_arg)
+
+ add_offense(send_node, :selector)
+ end
+ end
+
+ def create_table?(node)
+ method_name(node) == :create_table
+ end
+
+ def method_name(node)
+ node.children[1]
+ end
+
+ def first_argument(node)
+ node.children[2] ? node.children[0] : nil
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/column_with_default.rb b/rubocop/cop/migration/column_with_default.rb
new file mode 100644
index 00000000000..97ee8b11044
--- /dev/null
+++ b/rubocop/cop/migration/column_with_default.rb
@@ -0,0 +1,50 @@
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if columns are added in a way that doesn't require
+ # downtime.
+ class ColumnWithDefault < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ WHITELISTED_TABLES = [:application_settings]
+
+ MSG = 'add_column with a default value requires downtime, ' \
+ 'use add_column_with_default instead'
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_column
+
+ # Ignore whitelisted tables.
+ return if table_whitelisted?(node.children[2])
+
+ opts = node.children.last
+
+ return unless opts && opts.type == :hash
+
+ opts.each_node(:pair) do |pair|
+ if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
+ add_offense(node, :selector)
+ end
+ end
+ end
+
+ def table_whitelisted?(symbol)
+ symbol && symbol.type == :sym &&
+ WHITELISTED_TABLES.include?(symbol.children[0])
+ end
+
+ def hash_key_type(pair)
+ pair.children[0].type
+ end
+
+ def hash_key_name(pair)
+ pair.children[0].children[0]
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb
new file mode 100644
index 00000000000..3160a784a04
--- /dev/null
+++ b/rubocop/migration_helpers.rb
@@ -0,0 +1,10 @@
+module RuboCop
+ # Module containing helper methods for writing migration cops.
+ module MigrationHelpers
+ # Returns true if the given node originated from the db/migrate directory.
+ def in_migration?(node)
+ File.dirname(node.location.expression.source_buffer.name).
+ end_with?('db/migrate')
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
new file mode 100644
index 00000000000..7922e19768b
--- /dev/null
+++ b/rubocop/rubocop.rb
@@ -0,0 +1,3 @@
+require_relative 'migration_helpers'
+require_relative 'cop/migration/add_index'
+require_relative 'cop/migration/column_with_default'
diff --git a/scripts/merge-reports b/scripts/merge-reports
new file mode 100755
index 00000000000..f7b574001ac
--- /dev/null
+++ b/scripts/merge-reports
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+
+require 'json'
+require 'yaml'
+
+main_report_file = ARGV.shift
+unless main_report_file
+ puts 'usage: merge_reports <main-report> [extra reports...]'
+ exit 1
+end
+
+puts "Loading #{main_report_file}..."
+main_report = JSON.parse(File.read(main_report_file))
+new_report = main_report.dup
+
+ARGV.each do |report_file|
+ report = JSON.parse(File.read(report_file))
+
+ # Remove existing values
+ updates = report.delete_if do |key, value|
+ main_report[key] && main_report[key] == value
+ end
+ new_report.merge!(updates)
+
+ puts "Merged #{report_file} adding #{updates.size} results."
+end
+
+File.write(main_report_file, JSON.pretty_generate(new_report))
+puts "Saved #{main_report_file}."
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 247383aa46c..7e71a030901 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,21 +1,25 @@
#!/bin/bash
retry() {
- for i in $(seq 1 3); do
+ if eval "$@"; then
+ return 0
+ fi
+
+ for i in 2 1; do
+ sleep 3s
+ echo "Retrying $i..."
if eval "$@"; then
return 0
fi
- sleep 3s
- echo "Retrying..."
done
return 1
}
if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
- mkdir -p vendor
+ mkdir -p vendor/apt
# Install phantomjs package
- pushd vendor
+ pushd vendor/apt
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
diff --git a/shared/registry/.gitkeep b/shared/registry/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/registry/.gitkeep
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 462afb24f08..6fad7e2b9e7 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -43,7 +43,7 @@ describe "mail_room.yml" do
redis_config_file = Rails.root.join('config', 'resque.yml')
redis_url =
- if File.exists?(redis_config_file)
+ if File.exist?(redis_config_file)
YAML.load_file(redis_config_file)[Rails.env]
else
"redis://localhost:6379"
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index eb82476b179..d5f0b289b5b 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -22,7 +22,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "doesn't sign us in" do
@@ -46,7 +46,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "doesn't sign us in as the impersonator" do
@@ -65,7 +65,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "doesn't sign us in as the impersonator" do
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 2ba0d489197..8eaacef2024 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -11,13 +11,13 @@ describe Admin::ProjectsController do
render_views
it 'retrieves the project for the given visibility level' do
- get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]
+ get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC]
expect(response.body).to match(project.name)
end
it 'does not retrieve the project' do
- get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]
- expect(response.body).to_not match(project.name)
+ get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL]
+ expect(response.body).not_to match(project.name)
end
end
end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index b51b303a714..520a4f6f9c5 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -14,7 +14,7 @@ describe Admin::SpamLogsController do
it 'lists all spam logs' do
get :index
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -22,14 +22,14 @@ describe Admin::SpamLogsController 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)
+ expect(response).to have_http_status(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(response).to have_http_status(302)
expect(SpamLog.count).to eq(0)
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index ce2a62ae1fd..ab9aa65f7b9 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -17,7 +17,7 @@ describe Admin::UsersController do
it 'deletes user' do
delete :destroy, id: user.username, format: :json
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect { User.find(user.id) }.to raise_exception(ActiveRecord::RecordNotFound)
end
end
@@ -114,6 +114,82 @@ describe Admin::UsersController do
end
end
+ describe 'POST update' do
+ context 'when the password has changed' do
+ def update_password(user, password, password_confirmation = nil)
+ params = {
+ id: user.to_param,
+ user: {
+ password: password,
+ password_confirmation: password_confirmation || password
+ }
+ }
+
+ post :update, params
+ end
+
+ context 'when the new password is valid' do
+ it 'redirects to the user' do
+ update_password(user, 'AValidPassword1')
+
+ expect(response).to redirect_to(admin_user_path(user))
+ end
+
+ it 'updates the password' do
+ update_password(user, 'AValidPassword1')
+
+ expect { user.reload }.to change { user.encrypted_password }
+ end
+
+ it 'sets the new password to expire immediately' do
+ update_password(user, 'AValidPassword1')
+
+ expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now)
+ end
+ end
+
+ context 'when the new password is invalid' do
+ it 'shows the edit page again' do
+ update_password(user, 'invalid')
+
+ expect(response).to render_template(:edit)
+ end
+
+ it 'returns the error message' do
+ update_password(user, 'invalid')
+
+ expect(assigns[:user].errors).to contain_exactly(a_string_matching(/too short/))
+ end
+
+ it 'does not update the password' do
+ update_password(user, 'invalid')
+
+ expect { user.reload }.not_to change { user.encrypted_password }
+ end
+ end
+
+ context 'when the new password does not match the password confirmation' do
+ it 'shows the edit page again' do
+ update_password(user, 'AValidPassword1', 'AValidPassword2')
+
+ expect(response).to render_template(:edit)
+ end
+
+ it 'returns the error message' do
+ update_password(user, 'AValidPassword1', 'AValidPassword2')
+
+ expect(assigns[:user].errors).to contain_exactly(a_string_matching(/doesn't match/))
+ end
+
+ it 'does not update the password' do
+ update_password(user, 'AValidPassword1', 'AValidPassword2')
+
+ expect { user.reload }.not_to change { user.encrypted_password }
+ end
+ end
+ end
+ end
+
describe "POST impersonate" do
context "when the user is blocked" do
before do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 186239d3096..8bd210cbc3d 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -30,4 +30,74 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
end
+
+ describe "#authenticate_user_from_token!" do
+ describe "authenticating a user from a private token" do
+ controller(ApplicationController) do
+ def index
+ render text: "authenticated"
+ end
+ end
+
+ let(:user) { create(:user) }
+
+ context "when the 'private_token' param is populated with the private token" do
+ it "logs the user in" do
+ get :index, private_token: user.private_token
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+ context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
+ it "logs the user in" do
+ @request.headers['PRIVATE-TOKEN'] = user.private_token
+ get :index
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ @request.headers['PRIVATE-TOKEN'] = "token"
+ get :index, private_token: "token", authenticity_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq("authenticated")
+ end
+ end
+
+ describe "authenticating a user from a personal access token" do
+ controller(ApplicationController) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
+ end
+ end
+ end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 28cf804c1b2..60c654f622d 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -29,7 +29,7 @@ describe AutocompleteController do
get(:users, project_id: 'unknown')
end
- it { expect(response.status).to eq(404) }
+ it { expect(response).to have_http_status(404) }
end
end
@@ -58,7 +58,7 @@ describe AutocompleteController do
get(:users, group_id: 'unknown')
end
- it { expect(response.status).to eq(404) }
+ it { expect(response).to have_http_status(404) }
end
end
@@ -114,7 +114,7 @@ describe AutocompleteController do
get(:users, project_id: project.id)
end
- it { expect(response.status).to eq(404) }
+ it { expect(response).to have_http_status(404) }
end
describe 'GET #users with unknown project' do
@@ -122,7 +122,7 @@ describe AutocompleteController do
get(:users, project_id: 'unknown')
end
- it { expect(response.status).to eq(404) }
+ it { expect(response).to have_http_status(404) }
end
describe 'GET #users with inaccessible group' do
@@ -131,7 +131,7 @@ describe AutocompleteController do
get(:users, group_id: user.namespace.id)
end
- it { expect(response.status).to eq(404) }
+ it { expect(response).to have_http_status(404) }
end
describe 'GET #users with no project' do
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index eb91e577b87..465013231f9 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -38,6 +38,11 @@ describe Projects::BlobController do
let(:id) { 'invalid-branch/README.md' }
it { is_expected.to respond_with(:not_found) }
end
+
+ context "binary file" do
+ let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+ it { is_expected.to respond_with(:success) }
+ end
end
describe 'GET show with tree path' do
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index cf5c606c723..a3a3309e15e 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -155,7 +155,7 @@ describe Projects::CommitController do
id: commit.id)
expect(response).not_to be_success
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -204,7 +204,7 @@ describe Projects::CommitController do
id: master_pickable_commit.id)
expect(response).not_to be_success
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a5986598715..c34475976c6 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -4,17 +4,209 @@ describe Groups::GroupMembersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- context "index" do
+ describe '#index' do
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'renders index with group members' do
- get :index, group_id: group.path
+ get :index, group_id: group
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response).to render_template(:index)
end
end
+
+ describe '#destroy' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: 42
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:member) do
+ group.add_developer(group_user)
+ group.members.find_by(user_id: group_user)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response).to have_http_status(403)
+ expect(group.users).to include group_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response).to set_flash.to 'User was successfully removed from group.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).not_to include group_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, group_id: group,
+ id: member
+
+ expect(response).to be_success
+ expect(group.users).not_to include group_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, group_id: group
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to "You left the \"#{group.name}\" group."
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(group.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'cannot removes himself from the group' do
+ delete :leave, group_id: group
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ group.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
+ expect(response).to redirect_to(group_path(group))
+ expect(group.requesters).to be_empty
+ expect(group.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new GroupMember that is not a team member' do
+ post :request_access, group_id: group
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(group_path(group))
+ expect(group.requesters.exists?(user_id: user)).to be_truthy
+ expect(group.users).not_to include user
+ end
+ end
+
+ describe '#approve_access_request' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: 42
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_requester) { create(:user) }
+ let(:member) do
+ group.request_access(group_requester)
+ group.requesters.find_by(user_id: group_requester)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response).to have_http_status(403)
+ expect(group.users).not_to include group_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).to include group_requester
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb
deleted file mode 100644
index 0786e45515a..00000000000
--- a/spec/controllers/groups/notification_settings_controller_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Groups::NotificationSettingsController do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
-
- describe '#update' do
- context 'when not authorized' do
- it 'redirects to sign in page' do
- put :update,
- group_id: group.to_param,
- notification_setting: { level: :participating }
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when authorized' do
- before do
- sign_in(user)
- end
-
- it 'returns success' do
- put :update,
- group_id: group.to_param,
- notification_setting: { level: :participating }
-
- expect(response.status).to eq 200
- end
- end
- end
-end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 465531b2b36..cd98fecd0c7 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: issue_2)
- create_list(:upvote_note, 2, project: project, noteable: issue_1)
- create_list(:downvote_note, 2, project: project, noteable: issue_2)
+ create_list(:award_emoji, 3, awardable: issue_2)
+ create_list(:award_emoji, 2, awardable: issue_1)
+ create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
sign_in(user)
end
@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
- create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
- create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+ create_list(:award_emoji, 3, awardable: merge_request_2)
+ create_list(:award_emoji, 2, awardable: merge_request_1)
+ create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
sign_in(user)
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
new file mode 100644
index 00000000000..56ecf2bb644
--- /dev/null
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe HealthCheckController do
+ let(:token) { current_application_settings.health_check_access_token }
+ let(:json_response) { JSON.parse(response.body) }
+ let(:xml_response) { Hash.from_xml(response.body)['hash'] }
+
+ describe 'GET #index' do
+ context 'when services are up but NO access token' do
+ it 'returns a not found page' do
+ get :index
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when services are up and an access token is provided' do
+ it 'supports passing the token in the header' do
+ request.headers['TOKEN'] = token
+ get :index
+ expect(response).to be_success
+ expect(response.content_type).to eq 'text/plain'
+ end
+
+ it 'supports successful plaintest response' do
+ get :index, token: token
+ expect(response).to be_success
+ expect(response.content_type).to eq 'text/plain'
+ end
+
+ it 'supports successful json response' do
+ get :index, token: token, format: :json
+ expect(response).to be_success
+ expect(response.content_type).to eq 'application/json'
+ expect(json_response['healthy']).to be true
+ end
+
+ it 'supports successful xml response' do
+ get :index, token: token, format: :xml
+ expect(response).to be_success
+ expect(response.content_type).to eq 'application/xml'
+ expect(xml_response['healthy']).to be true
+ end
+
+ it 'supports successful responses for specific checks' do
+ get :index, token: token, checks: 'email', format: :json
+ expect(response).to be_success
+ expect(response.content_type).to eq 'application/json'
+ expect(json_response['healthy']).to be true
+ end
+ end
+
+ context 'when a service is down but NO access token' do
+ it 'returns a not found page' do
+ get :index
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when a service is down and an access token is provided' do
+ before do
+ allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire')
+ allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire')
+ end
+
+ it 'supports passing the token in the header' do
+ request.headers['TOKEN'] = token
+ get :index
+ expect(response).to have_http_status(500)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to include('The server is on fire')
+ end
+
+ it 'supports failure plaintest response' do
+ get :index, token: token
+ expect(response).to have_http_status(500)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to include('The server is on fire')
+ end
+
+ it 'supports failure json response' do
+ get :index, token: token, format: :json
+ expect(response).to have_http_status(500)
+ expect(response.content_type).to eq 'application/json'
+ expect(json_response['healthy']).to be false
+ expect(json_response['message']).to include('The server is on fire')
+ end
+
+ it 'supports failure xml response' do
+ get :index, token: token, format: :xml
+ expect(response).to have_http_status(500)
+ expect(response.content_type).to eq 'application/xml'
+ expect(xml_response['healthy']).to be false
+ expect(xml_response['message']).to include('The server is on fire')
+ end
+
+ it 'supports failure responses for specific checks' do
+ get :index, token: token, checks: 'email', format: :json
+ expect(response).to have_http_status(500)
+ expect(response.content_type).to eq 'application/json'
+ expect(json_response['healthy']).to be false
+ expect(json_response['message']).to include('Email is on fire')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 81c03c9059b..07bf8d2d1c3 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::BitbucketController do
include ImportSpecHelper
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index 27b11267d2a..5f0f6dea821 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::FogbugzController do
include ImportSpecHelper
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index bcc713dce2a..51d59526854 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GithubController do
include ImportSpecHelper
@@ -17,6 +16,24 @@ describe Import::GithubController do
allow(controller).to receive(:github_import_enabled?).and_return(true)
end
+ describe "GET new" do
+ it "redirects to GitHub for an access token if logged in with GitHub" do
+ allow(controller).to receive(:logged_in_with_github?).and_return(true)
+ expect(controller).to receive(:go_to_github_for_permissions)
+
+ get :new
+ end
+
+ it "redirects to status if we already have a token" do
+ assign_session_token
+ allow(controller).to receive(:logged_in_with_github?).and_return(false)
+
+ get :new
+
+ expect(controller).to redirect_to(status_import_github_url)
+ end
+ end
+
describe "GET callback" do
it "updates access token" do
token = "asdasd12345"
@@ -33,6 +50,20 @@ describe Import::GithubController do
end
end
+ describe "POST personal_access_token" do
+ it "updates access token" do
+ token = "asdfasdf9876"
+
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:user).and_return(true)
+
+ post :personal_access_token, personal_access_token: token
+
+ expect(session[:github_access_token]).to eq(token)
+ expect(controller).to redirect_to(status_import_github_url)
+ end
+ end
+
describe "GET status" do
before do
@repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim')
@@ -60,6 +91,17 @@ describe Import::GithubController do
expect(assigns(:already_added_projects)).to eq([@project])
expect(assigns(:repos)).to eq([])
end
+
+ it "handles an invalid access token" do
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:repos).and_raise(Octokit::Unauthorized)
+
+ get :status
+
+ expect(session[:github_access_token]).to eq(nil)
+ expect(controller).to redirect_to(new_import_github_url)
+ expect(flash[:alert]).to eq('Access denied to your GitHub account.')
+ end
end
describe "POST create" do
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 198d006af76..e8cf6aa7767 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GitlabController do
include ImportSpecHelper
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
index 7cb1b85a46d..4ae2b78e11c 100644
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ b/spec/controllers/import/gitorious_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GitoriousController do
include ImportSpecHelper
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
index 66088139a69..4241db6e771 100644
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GoogleCodeController do
include ImportSpecHelper
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index 3c6e54839b5..e478a253b3f 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -15,7 +15,7 @@ describe InvitesController do
get :accept, id: token
member.reload
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(member.user).to eq(user)
expect(flash[:notice]).to include 'You have been granted'
end
@@ -26,7 +26,7 @@ describe InvitesController do
get :decline, id: token
expect{member.reload}.to raise_error ActiveRecord::RecordNotFound
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(flash[:notice]).to include 'You have declined the invitation to join'
end
end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
index 27e9afe582e..2b334ed1172 100644
--- a/spec/controllers/namespaces_controller_spec.rb
+++ b/spec/controllers/namespaces_controller_spec.rb
@@ -86,7 +86,7 @@ describe NamespacesController do
it "responds with status 404" do
get :show, id: group.path
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -102,7 +102,7 @@ describe NamespacesController do
it "responds with status 404" do
get :show, id: "doesntexist"
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
new file mode 100644
index 00000000000..79b819a1377
--- /dev/null
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe NotificationSettingsController do
+ let(:project) { create(:empty_project) }
+ let(:group) { create(:group, :internal) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe '#create' do
+ context 'when not authorized' do
+ it 'redirects to sign in page' do
+ post :create,
+ project_id: project.id,
+ notification_setting: { level: :participating }
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when authorized' do
+ let(:custom_events) do
+ events = {}
+
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ events[event.to_s] = true
+ end
+
+ events
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'for projects' do
+ let(:notification_setting) { user.notification_settings_for(project) }
+
+ it 'creates notification setting' do
+ post :create,
+ project_id: project.id,
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq 200
+ expect(notification_setting.level).to eq("participating")
+ expect(notification_setting.user_id).to eq(user.id)
+ expect(notification_setting.source_id).to eq(project.id)
+ expect(notification_setting.source_type).to eq("Project")
+ end
+
+ context 'with custom settings' do
+ it 'creates notification setting' do
+ post :create,
+ project_id: project.id,
+ notification_setting: { level: :custom }.merge(custom_events)
+
+ expect(response.status).to eq 200
+ expect(notification_setting.level).to eq("custom")
+ expect(notification_setting.events).to eq(custom_events)
+ end
+ end
+ end
+
+ context 'for groups' do
+ let(:notification_setting) { user.notification_settings_for(group) }
+
+ it 'creates notification setting' do
+ post :create,
+ namespace_id: group.id,
+ notification_setting: { level: :watch }
+
+ expect(response.status).to eq 200
+ expect(notification_setting.level).to eq("watch")
+ expect(notification_setting.user_id).to eq(user.id)
+ expect(notification_setting.source_id).to eq(group.id)
+ expect(notification_setting.source_type).to eq("Namespace")
+ end
+
+ context 'with custom settings' do
+ it 'creates notification setting' do
+ post :create,
+ namespace_id: group.id,
+ notification_setting: { level: :custom }.merge(custom_events)
+
+ expect(response.status).to eq 200
+ expect(notification_setting.level).to eq("custom")
+ expect(notification_setting.events).to eq(custom_events)
+ end
+ end
+ end
+ end
+
+ context 'not authorized' do
+ let(:private_project) { create(:project, :private) }
+ before { sign_in(user) }
+
+ it 'returns 404' do
+ post :create,
+ project_id: private_project.id,
+ notification_setting: { level: :participating }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:notification_setting) { user.global_notification_setting }
+
+ context 'when not authorized' do
+ it 'redirects to sign in page' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when authorized' do
+ before{ sign_in(user) }
+
+ it 'returns success' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq 200
+ end
+
+ context 'and setting custom notification setting' do
+ let(:custom_events) do
+ events = {}
+
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ events[event] = "true"
+ end
+ end
+
+ it 'returns success' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating, events: custom_events }
+
+ expect(response.status).to eq 200
+ end
+ end
+ end
+
+ context 'not authorized' do
+ let(:other_user) { create(:user) }
+
+ before { sign_in(other_user) }
+
+ it 'returns 404' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
new file mode 100644
index 00000000000..552899eb36c
--- /dev/null
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Oauth::ApplicationsController do
+ let(:user) { create(:user) }
+
+ context 'project members' do
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'shows list of applications' do
+ get :index
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'redirects back to profile page if OAuth applications are disabled' do
+ settings = double(user_oauth_applications?: false)
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings)
+
+ get :index
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(profile_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
new file mode 100644
index 00000000000..18148acde3e
--- /dev/null
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Profiles::AccountsController do
+ let(:user) { create(:omniauth_user, provider: 'saml') }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'does not allow to unlink SAML connected account' do
+ identity = user.identities.last
+ delete :unlink, provider: 'saml'
+ updated_user = User.find(user.id)
+
+ expect(response).to have_http_status(302)
+ expect(updated_user.identities.size).to eq(1)
+ expect(updated_user.identities).to include(identity)
+ end
+
+ it 'does allow to delete other linked accounts' do
+ user.identities.create(provider: 'twitter', extern_uid: 'twitter_123')
+
+ expect { delete :unlink, provider: 'twitter' }.to change(Identity.all, :size).by(-1)
+ end
+end
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 4fb1473c2d2..d08d0018b35 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do
allow(subject).to receive(:current_user).and_return(user)
end
- describe 'GET new' do
+ describe 'GET show' do
let(:user) { create(:user) }
it 'generates otp_secret for user' do
expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once
- get :new
- get :new # Second hit shouldn't re-generate it
+ get :show
+ get :show # Second hit shouldn't re-generate it
end
it 'assigns qr_code' do
code = double('qr code')
expect(subject).to receive(:build_qr_code).and_return(code)
- get :new
+ get :show
expect(assigns[:qr_code]).to eq code
end
end
@@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do
expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end
- it 'sets two_factor_enabled' do
+ it 'enables 2fa for the user' do
go
user.reload
@@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do
expect(assigns[:qr_code]).to eq code
end
- it 'renders new' do
+ it 'renders show' do
go
- expect(response).to render_template(:new)
+ expect(response).to render_template(:show)
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
new file mode 100644
index 00000000000..9444a50b1ce
--- /dev/null
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -0,0 +1,40 @@
+require 'rails_helper'
+
+describe Projects::BlobController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ user = create(:user)
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET diff' do
+ render_views
+
+ def do_get(opts = {})
+ params = { namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: 'master/CHANGELOG' }
+ get :diff, params.merge(opts)
+ end
+
+ context 'when essential params are missing' do
+ it 'renders nothing' do
+ do_get
+
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'when essential params are present' do
+ it 'renders the diff content' do
+ do_get(since: 1, to: 5, offset: 10)
+
+ expect(response.body).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 8ad73472117..644de308c64 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -68,7 +68,6 @@ describe Projects::BranchesController do
let(:branch) { "1-feature-branch" }
let!(:issue) { create(:issue, project: project) }
-
it 'redirects' do
post :create,
namespace_id: project.namespace.to_param,
@@ -89,7 +88,6 @@ describe Projects::BranchesController do
branch_name: branch,
issue_iid: issue.iid
end
-
end
end
@@ -103,7 +101,7 @@ describe Projects::BranchesController do
namespace_id: project.namespace.to_param,
project_id: project.to_param
- expect(response.status).to eq(303)
+ expect(response).to have_http_status(303)
end
end
@@ -121,28 +119,24 @@ describe Projects::BranchesController do
context "valid branch name, valid source" do
let(:branch) { "feature" }
- it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
+ it { expect(response).to have_http_status(200) }
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') }
+ it { expect(response).to have_http_status(200) }
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') }
+ it { expect(response).to have_http_status(200) }
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') }
+ it { expect(response).to have_http_status(404) }
end
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 438e776ec4b..6e3db10e451 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -2,6 +2,8 @@ require 'rails_helper'
describe Projects::CommitController do
describe 'GET show' do
+ render_views
+
let(:project) { create(:project) }
before do
@@ -27,6 +29,16 @@ describe Projects::CommitController do
end
end
+ it 'handles binary files' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: TestEnv::BRANCH_SHA['binary-encoding'],
+ format: "html")
+
+ expect(response).to be_success
+ end
+
def go(id:)
get :show,
namespace_id: project.namespace.to_param,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 788a609ee40..4018dac95a2 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).first).to_not be_nil
+ expect(assigns(:diffs).first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
end
@@ -32,7 +32,7 @@ describe Projects::CompareController do
w: 1)
expect(response).to be_success
- expect(assigns(:diffs).first).to_not be_nil
+ expect(assigns(:diffs).first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
# without whitespace option, there are more than 2 diff_splits
diff_splits = assigns(:diffs).first.diff.split("\n")
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 70ed8f3a62e..f66bcb8099c 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -64,9 +64,7 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
end
-
end
end
end
-
end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 40bd83af861..fbe8758dda7 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -28,7 +28,7 @@ describe Projects::GroupLinksController do
expect(group.shared_projects).to include project
end
- it 'redirects to project group links page'do
+ it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
)
@@ -43,7 +43,7 @@ describe Projects::GroupLinksController do
end
it 'does not share project with that group' do
- expect(group.shared_projects).to_not include project
+ expect(group.shared_projects).not_to include project
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 2b2ad3b9412..7cf09fa4a4a 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::IssuesController do
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "return 301 if request path doesn't match project path" do
@@ -28,7 +28,7 @@ describe Projects::IssuesController do
project.save
get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "returns 404 when external issue tracker is enabled" do
@@ -36,7 +36,7 @@ describe Projects::IssuesController do
allow(project).to receive(:default_issues_tracker?).and_return(false)
get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -56,7 +56,7 @@ describe Projects::IssuesController do
move_issue
expect(response).to have_http_status :found
- expect(another_project.issues).to_not be_empty
+ expect(another_project.issues).not_to be_empty
end
end
@@ -105,6 +105,15 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
+ it 'should not list confidential issues for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
it 'should list confidential issues for author' do
sign_in(author)
get_issues
@@ -148,7 +157,7 @@ describe Projects::IssuesController do
shared_examples_for 'restricted action' do |http_status|
it 'returns 404 for guests' do
- sign_out :user
+ sign_out(:user)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_http_status :not_found
@@ -161,6 +170,14 @@ describe Projects::IssuesController do
expect(response).to have_http_status :not_found
end
+ it 'returns 404 for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+ 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)
@@ -231,7 +248,7 @@ describe Projects::IssuesController do
before { sign_in(user) }
it "rejects a developer to destroy an issue" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -245,9 +262,25 @@ describe Projects::IssuesController do
it "deletes the issue" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
end
end
end
+
+ describe 'POST #toggle_award_emoji' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: issue.iid, name: "thumbsup")
+ end.to change { issue.award_emoji.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
new file mode 100644
index 00000000000..3492b6ffbbb
--- /dev/null
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Projects::LabelsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ def create_label(attributes)
+ create(:label, attributes.merge(project: project))
+ end
+
+ before do
+ 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
+ 5.times { |i| create_label(title: "label #{100 - i}") }
+
+ get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ end
+
+ context '@prioritized_labels' do
+ let(:prioritized_labels) { assigns(:prioritized_labels) }
+
+ it 'contains only prioritized labels' do
+ expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
+ end
+
+ it 'is sorted by priority, then label title' do
+ priorities_and_titles = prioritized_labels.pluck(:priority, :title)
+
+ expect(priorities_and_titles.sort).to eq(priorities_and_titles)
+ end
+ end
+
+ context '@labels' do
+ let(:labels) { assigns(:labels) }
+
+ it 'contains only unprioritized labels' do
+ expect(labels).to all(have_attributes(priority: nil))
+ end
+
+ it 'is sorted by label title' do
+ titles = labels.pluck(:title)
+
+ expect(titles.sort).to eq(titles)
+ 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 c0a1f45195f..c4b57e77804 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid,
format: format)
- expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
+ expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
end
it "should not escape Html" do
@@ -84,41 +84,26 @@ describe Projects::MergeRequestsController do
end
describe "as diff" do
- include_examples "export merge as", :diff
- let(:format) { :diff }
-
- it "should really only be a git diff" do
+ it "triggers workhorse to serve the request" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
- format: format)
+ format: :diff)
- expect(response.body).to start_with("diff --git")
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
describe "as patch" do
- include_examples "export merge as", :patch
- let(:format) { :patch }
-
- it "should really be a git email patch with commit" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid, format: format)
-
- expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}")
- end
-
- it "should contain git diffs" do
+ it 'triggers workhorse to serve the request' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
- format: format)
+ format: :patch)
- expect(response.body).to match(/^diff --git/)
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
end
end
@@ -132,7 +117,6 @@ describe Projects::MergeRequestsController do
end
context 'when filtering by opened state' do
-
context 'with opened merge requests' do
it 'should list those merge requests' do
get_merge_requests
@@ -153,7 +137,6 @@ describe Projects::MergeRequestsController do
expect(assigns(:merge_requests)).to include(merge_request)
end
end
-
end
end
@@ -185,11 +168,109 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'POST #merge' do
+ let(:base_params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ format: 'raw'
+ }
+ end
+
+ context 'when the user does not have access' do
+ before do
+ project.team.truncate
+ project.team << [user, :reporter]
+ post :merge, base_params
+ end
+
+ it 'returns not found' do
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when the merge request is not mergeable' do
+ before do
+ merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+ post :merge, base_params
+ end
+
+ it 'returns :failed' do
+ expect(assigns(:status)).to eq(:failed)
+ end
+ end
+
+ context 'when the sha parameter does not match the source SHA' do
+ before { post :merge, base_params.merge(sha: 'foo') }
+
+ it 'returns :sha_mismatch' do
+ expect(assigns(:status)).to eq(:sha_mismatch)
+ end
+ end
+
+ context 'when the sha parameter matches the source SHA' do
+ def merge_with_sha
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha)
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(assigns(:status)).to eq(:success)
+ end
+
+ it 'starts the merge immediately' do
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+
+ merge_with_sha
+ end
+
+ context 'when merge_when_build_succeeds is passed' do
+ def merge_when_build_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1')
+ end
+
+ before do
+ create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ end
+
+ it 'returns :merge_when_build_succeeds' do
+ merge_when_build_succeeds
+
+ expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ end
+
+ it 'sets the MR to merge when the build succeeds' do
+ service = double(:merge_when_build_succeeds_service)
+
+ expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service)
+ expect(service).to receive(:execute).with(merge_request)
+
+ merge_when_build_succeeds
+ end
+
+ context 'when project.only_allow_merge_if_build_succeeds? is true' do
+ before do
+ project.update_column(:only_allow_merge_if_build_succeeds, true)
+ end
+
+ it 'returns :merge_when_build_succeeds' do
+ merge_when_build_succeeds
+
+ expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ end
+ end
+ end
+ end
+ end
+
describe "DELETE #destroy" do
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
context "when the user is owner" do
@@ -202,7 +283,7 @@ describe Projects::MergeRequestsController do
it "deletes the merge request" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
new file mode 100644
index 00000000000..75590c1ed4f
--- /dev/null
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -0,0 +1,36 @@
+require('spec_helper')
+
+describe Projects::NotesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note, noteable: issue, project: project) }
+
+ describe 'POST #toggle_award_emoji' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+ end.to change { note.award_emoji.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "removes the already awarded emoji" do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+ end.to change { AwardEmoji.count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb
deleted file mode 100644
index 4908b545648..00000000000
--- a/spec/controllers/projects/notification_settings_controller_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-
-describe Projects::NotificationSettingsController do
- let(:project) { create(:empty_project) }
- let(:user) { create(:user) }
-
- before do
- project.team << [user, :developer]
- end
-
- describe '#update' do
- context 'when not authorized' do
- it 'redirects to sign in page' do
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- notification_setting: { level: :participating }
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when authorized' do
- before do
- sign_in(user)
- end
-
- it 'returns success' do
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- notification_setting: { level: :participating }
-
- expect(response.status).to eq 200
- end
- end
- end
-end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index ed64e7cf9af..5e2a8cf3849 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,22 +1,22 @@
require('spec_helper')
describe Projects::ProjectMembersController do
- let(:project) { create(:project) }
- let(:another_project) { create(:project, :private) }
- let(:user) { create(:user) }
- let(:member) { create(:user) }
-
- before do
- project.team << [user, :master]
- another_project.team << [member, :guest]
- sign_in(user)
- end
-
describe '#apply_import' do
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
shared_context 'import applied' do
before do
- post(:apply_import, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:apply_import, namespace_id: project.namespace,
+ project_id: project,
source_project_id: another_project.id)
end
end
@@ -38,7 +38,7 @@ describe Projects::ProjectMembersController do
include_context 'import applied'
it 'does not import team members' do
- expect(project.team_members).to_not include member
+ expect(project.team_members).not_to include member
end
it 'responds with not found' do
@@ -48,18 +48,227 @@ describe Projects::ProjectMembersController do
end
describe '#index' do
- let(:project) { create(:project, :private) }
-
context 'when user is member' do
- let(:member) { create(:user) }
-
before do
+ project = create(:project, :private)
+ member = create(:user)
project.team << [member, :guest]
sign_in(member)
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ get :index, namespace_id: project.namespace, project_id: project
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+ end
+
+ describe '#destroy' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_user) { create(:user) }
+ let(:member) do
+ project.team << [team_user, :developer]
+ project.members.find_by(user_id: team_user.id)
end
- it { expect(response.status).to eq(200) }
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to have_http_status(404)
+ expect(project.users).to include team_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).not_to include team_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to be_success
+ expect(project.users).not_to include team_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to "You left the \"#{project.human_name}\" project."
+ expect(response).to redirect_to(dashboard_projects_path)
+ expect(project.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ project.update(namespace_id: user.namespace_id)
+ project.team << [user, :master, user]
+ sign_in(user)
+ end
+
+ it 'cannot remove himself from the project' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ project.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
+ expect(response).to redirect_to(namespace_project_path(project.namespace, project))
+ expect(project.requesters).to be_empty
+ expect(project.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new ProjectMember that is not a team member' do
+ post :request_access, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(
+ namespace_project_path(project.namespace, project)
+ )
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+ expect(project.users).not_to include user
+ end
+ end
+
+ describe '#approve' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_requester) { create(:user) }
+ let(:member) do
+ project.request_access(team_requester)
+ project.requesters.find_by(user_id: team_requester.id)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to have_http_status(404)
+ expect(project.users).not_to include team_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).to include team_requester
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 1caa476d37d..48f799d8ca1 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -13,10 +13,11 @@ describe Projects::RawController do
project_id: public_project.to_param,
id: id)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).
to eq("inline")
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
@@ -29,8 +30,9 @@ describe Projects::RawController do
project_id: public_project.to_param,
id: id)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
@@ -42,7 +44,7 @@ describe Projects::RawController do
before do
public_project.lfs_objects << lfs_object
allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.render nothing: true }
+ allow(controller).to receive(:send_file) { controller.head :ok }
end
it 'serves the file' do
@@ -52,7 +54,7 @@ describe Projects::RawController do
project_id: public_project.to_param,
id: id)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -63,7 +65,7 @@ describe Projects::RawController do
project_id: public_project.to_param,
id: id)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 0ddbec9eac2..2fe3c263524 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -20,14 +20,14 @@ describe Projects::RepositoriesController do
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 "uses Gitlab::Workhorse" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
context "when the service raises an error" do
-
before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
end
@@ -35,7 +35,7 @@ describe Projects::RepositoriesController do
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).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 0f32a30f18b..b8a28f43707 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).not_to include(project_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -30,7 +30,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).to include(project_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -41,7 +41,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).to include(project_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -56,7 +56,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -67,7 +67,7 @@ describe Projects::SnippetsController do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -78,7 +78,7 @@ describe Projects::SnippetsController do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -88,7 +88,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -98,7 +98,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
new file mode 100644
index 00000000000..5a8bba28594
--- /dev/null
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -0,0 +1,102 @@
+require('spec_helper')
+
+describe Projects::TodosController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ context 'Issues' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for issue' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for issue that user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'should not create todo for issue when user not logged in' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+ end
+
+ context 'Merge Requests' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for merge request' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for merge request user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'should not create todo for merge request user has no access to' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index e74731c9ed8..1cc050247c6 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -64,9 +64,8 @@ describe Projects::TreeController do
context "valid SHA commit ID with path" do
let(:id) { '6d39438/.gitignore' }
- it { expect(response.status).to eq(302) }
+ it { expect(response).to have_http_status(302) }
end
-
end
describe 'GET show with blob path' do
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 93c4494c660..0893ee89f6a 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::UploadsController do
namespace_id: project.namespace.to_param,
project_id: project.to_param,
format: :json
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
@@ -79,7 +79,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -87,7 +87,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -106,7 +106,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -114,7 +114,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -140,7 +140,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -192,7 +192,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -224,7 +224,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -232,7 +232,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -253,7 +253,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -261,7 +261,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -270,7 +270,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 069cd917e5a..1b1b1bdf52d 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -8,6 +8,40 @@ describe ProjectsController do
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
describe "GET show" do
+ context "user not project member" do
+ before { sign_in(user) }
+
+ context "user does not have access to project" do
+ let(:private_project) { create(:project, :private) }
+
+ it "does not initialize notification setting" do
+ get :show, namespace_id: private_project.namespace.path, id: private_project.path
+ expect(assigns(:notification_setting)).to be_nil
+ end
+ end
+
+ context "user has access to project" do
+ context "and does not have notification setting" do
+ it "initializes notification as disabled" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(assigns(:notification_setting).level).to eq("global")
+ end
+ end
+
+ context "and has notification setting" do
+ before do
+ setting = user.notification_settings_for(public_project)
+ setting.level = :watch
+ setting.save
+ end
+
+ it "shows current notification setting" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(assigns(:notification_setting).level).to eq("watch")
+ end
+ end
+ end
+ end
context "rendering default project view" do
render_views
@@ -43,7 +77,7 @@ describe ProjectsController do
get :show, namespace_id: public_project.namespace.path, id: public_project.path
expect(assigns(:project)).to eq(public_project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -55,19 +89,16 @@ describe ProjectsController do
expect(response).to redirect_to("/#{public_project.path_with_namespace}")
end
-
# MySQL queries are case insensitive by default, so this spec would fail.
if Gitlab::Database.postgresql?
context "when there is also a match with the same casing" do
-
let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) }
it "loads the exactly matched project" do
-
get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
expect(assigns(:project)).to eq(other_project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -81,6 +112,17 @@ describe ProjectsController do
expect(public_project_with_dot_atom).not_to be_valid
end
end
+
+ context 'when the project is pending deletions' do
+ it 'renders a 404 error' do
+ project = create(:project, pending_delete: true)
+ sign_in(user)
+
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response.status).to eq 404
+ end
+ end
end
describe "#update" do
@@ -101,7 +143,7 @@ describe ProjectsController do
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -116,7 +158,7 @@ describe ProjectsController do
delete :destroy, namespace_id: project.namespace.path, id: project.path
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
@@ -189,7 +231,27 @@ describe ProjectsController do
delete(:remove_fork,
namespace_id: project.namespace.to_param,
id: project.to_param, format: :js)
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ describe "GET refs" do
+ it "should get a list of branches and tags" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to be_nil
+ end
+
+ it "should get a list of branches, tags and commits" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to include("123456")
end
end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
new file mode 100644
index 00000000000..026f41c926b
--- /dev/null
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe RegistrationsController do
+ describe '#create' do
+ around(:each) do |example|
+ perform_enqueued_jobs do
+ example.run
+ end
+ end
+
+ let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } }
+
+ context 'when sending email confirmation' do
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
+
+ it 'logs user in directly' do
+ expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size }
+ expect(subject.current_user).not_to be_nil
+ end
+ end
+
+ context 'when not sending email confirmation' do
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
+
+ it 'does not authenticate user and sends confirmation email' do
+ post(:create, user_params)
+ expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email])
+ expect(subject.current_user).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 83cc8ec6d26..4e9bfb0c69b 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -12,7 +12,7 @@ describe SessionsController do
post(:create, user: { login: 'invalid', password: 'invalid' })
expect(response)
- .to set_flash.now[:alert].to /Invalid login or password/
+ .to set_flash.now[:alert].to /Invalid Login or password/
end
end
@@ -25,16 +25,42 @@ describe SessionsController do
expect(response).to set_flash.to /Signed in successfully/
expect(subject.current_user). to eq user
end
+
+ it "creates an audit log record" do
+ expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("standard")
+ end
end
end
- context 'when using two-factor authentication' do
+ context 'when using two-factor authentication via OTP' do
let(:user) { create(:user, :two_factor) }
def authenticate_2fa(user_params)
post(:create, { user: user_params }, { otp_user_id: user.id })
end
+ context 'remember_me field' do
+ it 'sets a remember_user_token cookie when enabled' do
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).
+ to receive(:remember_me).with(user).and_call_original
+
+ authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp)
+
+ expect(response.cookies['remember_user_token']).to be_present
+ end
+
+ it 'does nothing when disabled' do
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).not_to receive(:remember_me)
+
+ authenticate_2fa(remember_me: '0', otp_attempt: user.current_otp)
+
+ expect(response.cookies['remember_user_token']).to be_nil
+ end
+ end
+
##
# See #14900 issue
#
@@ -47,7 +73,7 @@ describe SessionsController do
authenticate_2fa(login: another_user.username,
otp_attempt: another_user.current_otp)
- expect(subject.current_user).to_not eq another_user
+ expect(subject.current_user).not_to eq another_user
end
end
@@ -56,7 +82,7 @@ describe SessionsController do
authenticate_2fa(login: another_user.username,
otp_attempt: 'invalid')
- expect(subject.current_user).to_not eq another_user
+ expect(subject.current_user).not_to eq another_user
end
end
@@ -73,7 +99,7 @@ describe SessionsController do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
- expect(subject.current_user).to_not eq user
+ expect(subject.current_user).not_to eq user
end
it 'warns about invalid OTP code' do
@@ -96,6 +122,25 @@ describe SessionsController do
end
end
end
+
+ it "creates an audit log record" do
+ expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("two-factor")
+ end
+ end
+
+ context 'when using two-factor authentication via U2F device' do
+ let(:user) { create(:user, :two_factor) }
+
+ def authenticate_2fa_u2f(user_params)
+ post(:create, { user: user_params }, { otp_user_id: user.id })
+ end
+
+ it "creates an audit log record" do
+ allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
+ end
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index b3dcb52c500..2a89159c070 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -19,7 +19,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: other_personal_snippet.to_param
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -28,7 +28,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -54,7 +54,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -79,7 +79,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -88,7 +88,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -102,7 +102,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -110,7 +110,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -134,7 +134,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :raw, id: other_personal_snippet.to_param
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -143,7 +143,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -169,7 +169,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -194,7 +194,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -203,7 +203,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -217,7 +217,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :raw, id: 'doesntexist'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -225,7 +225,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :raw, id: 'doesntexist'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 73858e6f063..69124ab06bf 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -26,7 +26,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -35,7 +35,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -52,7 +52,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -64,7 +64,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -109,7 +109,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -118,7 +118,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -133,7 +133,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -145,7 +145,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -181,7 +181,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -190,7 +190,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -210,7 +210,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -222,7 +222,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -267,7 +267,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -276,7 +276,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 8045c8b940d..54a2d3d9460 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -33,7 +33,7 @@ describe UsersController do
it 'renders the show template' do
get :show, username: user.username
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response).to render_template('show')
end
end
@@ -47,7 +47,7 @@ describe UsersController do
context 'when logged out' do
it 'renders 404' do
get :show, username: user.username
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -56,7 +56,7 @@ describe UsersController do
it 'renders show' do
get :show, username: user.username
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response).to render_template('show')
end
end
@@ -64,7 +64,6 @@ describe UsersController do
end
describe 'GET #calendar' do
-
it 'renders calendar' do
sign_in(user)
@@ -112,4 +111,26 @@ describe UsersController do
expect(response).to render_template('calendar_activities')
end
end
+
+ describe 'GET #snippets' do
+ before do
+ sign_in(user)
+ end
+
+ context 'format html' do
+ it 'renders snippets page' do
+ get :snippets, username: user.username
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('show')
+ end
+ end
+
+ context 'format json' do
+ it 'response with snippets json data' do
+ get :snippets, username: user.username, format: :json
+ expect(response).to have_http_status(200)
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
+ end
+ end
end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
new file mode 100644
index 00000000000..4b858df52c9
--- /dev/null
+++ b/spec/factories/award_emoji.rb
@@ -0,0 +1,12 @@
+FactoryGirl.define do
+ factory :award_emoji do
+ name "thumbsup"
+ user
+ awardable factory: :issue
+
+ trait :upvote
+ trait :downvote do
+ name "thumbsdown"
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index cd49e559b7d..fe05a0cfc00 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -16,7 +16,7 @@ FactoryGirl.define do
}
end
- commit factory: :ci_commit
+ pipeline factory: :ci_pipeline
trait :success do
status 'success'
@@ -43,7 +43,7 @@ FactoryGirl.define do
end
after(:build) do |build, evaluator|
- build.project = build.commit.project
+ build.project = build.pipeline.project
end
factory :ci_not_started_build do
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/pipelines.rb
index 645cd7ae766..a039bef6f3c 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -17,30 +17,30 @@
#
FactoryGirl.define do
- factory :ci_empty_commit, class: Ci::Commit do
+ factory :ci_empty_pipeline, class: Ci::Pipeline do
sha '97de212e80737a608d939f648d959671fb0a0142'
project factory: :empty_project
- factory :ci_commit_without_jobs do
+ factory :ci_pipeline_without_jobs do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) }
end
end
- factory :ci_commit_with_one_job do
+ factory :ci_pipeline_with_one_job do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) }
end
end
- factory :ci_commit_with_two_jobs do
+ factory :ci_pipeline_with_two_job do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
end
end
- factory :ci_commit do
+ factory :ci_pipeline do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index b7c2b32cb13..1e5c479616c 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -3,12 +3,12 @@ FactoryGirl.define do
name 'default'
status 'success'
description 'commit status'
- commit factory: :ci_commit_with_one_job
+ pipeline factory: :ci_pipeline_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
+ build.project = build.pipeline.project
end
factory :generic_commit_status, class: GenericCommitStatus do
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
new file mode 100644
index 00000000000..82591604fcb
--- /dev/null
+++ b/spec/factories/deployments.rb
@@ -0,0 +1,13 @@
+FactoryGirl.define do
+ factory :deployment, class: Deployment do
+ sha '97de212e80737a608d939f648d959671fb0a0142'
+ ref 'master'
+ tag false
+
+ environment factory: :environment
+
+ after(:build) do |deployment, evaluator|
+ deployment.project = deployment.environment.project
+ end
+ end
+end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
new file mode 100644
index 00000000000..07265c26ca3
--- /dev/null
+++ b/spec/factories/environments.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :environment, class: Environment do
+ sequence(:name) { |n| "environment#{n}" }
+
+ project factory: :empty_project
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 840b13196a6..83e38095feb 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -7,51 +7,68 @@ FactoryGirl.define do
project
note "Note"
author
+ on_issue
factory :note_on_commit, traits: [:on_commit]
- factory :note_on_commit_diff, traits: [:on_commit, :on_diff]
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
- 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]
+
+ factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
+ factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote
+
+ factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
+ position do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: noteable.diff_refs
+ )
+ end
+ end
+
+ factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
+ position do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: project.commit(commit_id).try(:diff_refs)
+ )
+ end
+ end
trait :on_commit do
- project
+ noteable nil
+ noteable_type 'Commit'
+ noteable_id nil
commit_id RepoHelpers.sample_commit.id
- noteable_type "Commit"
end
- trait :on_diff do
+ trait :legacy_diff_note do
line_code "0_184_184"
end
- trait :on_merge_request do
- project
- noteable_id 1
- noteable_type "MergeRequest"
+ trait :on_issue do
+ noteable { create(:issue, project: project) }
end
- trait :on_issue do
- noteable_id 1
- noteable_type "Issue"
+ trait :on_merge_request do
+ noteable { create(:merge_request, source_project: project) }
end
trait :on_project_snippet do
- noteable_id 1
- noteable_type "Snippet"
+ noteable { create(:snippet, project: project) }
end
trait :system do
system true
end
- trait :award do
- is_award true
- end
-
trait :downvote do
note "thumbsdown"
end
diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb
new file mode 100644
index 00000000000..b5e96d18b8f
--- /dev/null
+++ b/spec/factories/notification_settings.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :notification_setting do
+ source factory: :empty_project
+ user
+ level 3
+ events []
+ end
+end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
new file mode 100644
index 00000000000..da4c72bcb5b
--- /dev/null
+++ b/spec/factories/personal_access_tokens.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :personal_access_token do
+ user
+ token { SecureRandom.hex(50) }
+ name { FFaker::Product.brand }
+ revoked false
+ expires_at { 5.days.from_now }
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index da8d97c9f82..5c8ddbebf0d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -67,9 +67,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
-
- project.issues_tracker = 'redmine'
- project.issues_tracker_id = 'project_name_in_redmine'
end
end
@@ -84,9 +81,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
}
)
-
- project.issues_tracker = 'jira'
- project.issues_tracker_id = 'project_name_in_jira'
end
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index e3681ae93a5..7fc20cd5555 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -18,5 +18,13 @@ FactoryGirl.define do
commit_id RepoHelpers.sample_commit.id
target_type "Commit"
end
+
+ trait :build_failed do
+ action { Todo::BUILD_FAILED }
+ end
+
+ trait :done do
+ state :done
+ end
end
end
diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb
new file mode 100644
index 00000000000..df92b079581
--- /dev/null
+++ b/spec/factories/u2f_registrations.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :u2f_registration do
+ certificate { FFaker::BaconIpsum.characters(728) }
+ key_handle { FFaker::BaconIpsum.characters(86) }
+ public_key { FFaker::BaconIpsum.characters(88) }
+ counter 0
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index a9b2148bd2a..c6f7869516e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -15,14 +15,26 @@ FactoryGirl.define do
end
trait :two_factor do
+ two_factor_via_otp
+ end
+
+ trait :two_factor_via_otp do
before(:create) do |user|
- user.two_factor_enabled = true
+ user.otp_required_for_login = true
user.otp_secret = User.generate_otp_secret(32)
user.otp_grace_period_started_at = Time.now
user.generate_otp_backup_codes!
end
end
+ trait :two_factor_via_u2f do
+ transient { registrations_count 5 }
+
+ after(:create) do |user, evaluator|
+ create_list(:u2f_registration, evaluator.registrations_count, user: user)
+ end
+ end
+
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index 938ccf2306b..efa6cbe5bb1 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -2,7 +2,7 @@ require 'ostruct'
FactoryGirl.define do
factory :wiki_page do
- page = OpenStruct.new(url_path: 'some-name')
+ page { OpenStruct.new(url_path: 'some-name') }
association :wiki, factory: :project_wiki, strategy: :build
initialize_with { new(wiki, page, true) }
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 62de081661d..675d9bd18b7 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -5,8 +5,8 @@ describe 'factories' do
describe "#{factory.name} factory" do
let(:entity) { build(factory.name) }
- it 'does not raise error when created 'do
- expect { entity }.to_not raise_error
+ it 'does not raise error when created' do
+ expect { entity }.not_to raise_error
end
it 'should be valid', if: factory.build_class < ActiveRecord::Base do
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
new file mode 100644
index 00000000000..16baf7e9516
--- /dev/null
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe "Admin::AbuseReports", feature: true, js: true do
+ let(:user) { create(:user) }
+
+ context 'as an admin' do
+ describe 'if a user has been reported for abuse' do
+ before do
+ create(:abuse_report, user: user)
+ login_as :admin
+ end
+
+ describe 'in the abuse report view' do
+ it "should present a link to the user's profile" do
+ visit admin_abuse_reports_path
+
+ expect(page).to have_link user.name, href: user_path(user)
+ end
+ end
+
+ describe 'in the profile page of the user' do
+ it 'should show a link to the admin view of the user' do
+ visit user_path(user)
+
+ expect(page).to have_link '', href: admin_user_path(user)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 2e9851fb442..a6198389f04 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -6,19 +6,20 @@ describe 'Admin Builds' do
end
describe 'GET /admin/builds' do
- let(:commit) { create(:ci_commit) }
+ let(:pipeline) { create(:ci_pipeline) }
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)
+ create(:ci_build, pipeline: pipeline, status: :pending)
+ create(:ci_build, pipeline: pipeline, status: :running)
+ create(:ci_build, pipeline: pipeline, status: :success)
+ create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
+ expect(page).to have_selector('.row-content-block', text: 'All builds')
expect(page.all('.build-link').size).to eq(4)
expect(page).to have_link 'Cancel all'
end
@@ -38,9 +39,9 @@ describe 'Admin Builds' do
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)
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :success)
+ build3 = create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path(scope: :running)
@@ -54,7 +55,7 @@ describe 'Admin Builds' do
context 'when have no builds running' do
it 'shows a message' do
- create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running)
@@ -68,9 +69,9 @@ describe 'Admin Builds' do
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)
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :running)
+ build3 = create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :finished)
@@ -84,7 +85,7 @@ describe 'Admin Builds' do
context 'when have no builds finished' do
it 'shows a message' do
- create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished)
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
new file mode 100644
index 00000000000..5b1c0460274
--- /dev/null
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+feature 'Admin disables Git access protocol', feature: true do
+ let(:project) { create(:empty_project, :empty_repo) }
+ let(:admin) { create(:admin) }
+
+ background do
+ login_as(admin)
+ end
+
+ context 'with HTTP disabled' do
+ background do
+ disable_http_protocol
+ end
+
+ scenario 'shows only SSH url' do
+ visit_project
+
+ expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
+ expect(page).not_to have_selector('#clone-dropdown')
+ end
+ end
+
+ context 'with SSH disabled' do
+ background do
+ disable_ssh_protocol
+ end
+
+ scenario 'shows only HTTP url' do
+ visit_project
+
+ expect(page).to have_content("git clone #{project.http_url_to_repo}")
+ expect(page).not_to have_selector('#clone-dropdown')
+ end
+ end
+
+ context 'with nothing disabled' do
+ background do
+ create(:personal_key, user: admin)
+ end
+
+ scenario 'shows default SSH url and protocol selection dropdown' do
+ visit_project
+
+ expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
+ expect(page).to have_selector('#clone-dropdown')
+ end
+
+ end
+
+ def visit_project
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ def disable_http_protocol
+ visit admin_application_settings_path
+ find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[2]').select_option
+ click_on 'Save'
+ end
+
+ def disable_ssh_protocol
+ visit admin_application_settings_path
+ find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[3]').select_option
+ click_on 'Save'
+ end
+end
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
new file mode 100644
index 00000000000..dec2dedf2b5
--- /dev/null
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature "Admin Health Check", feature: true do
+ include WaitForAjax
+
+ before do
+ login_as :admin
+ end
+
+ describe '#show' do
+ before do
+ visit admin_health_check_path
+ end
+
+ it { page.has_text? 'Health Check' }
+ it { page.has_text? 'Health information can be retrieved' }
+
+ it 'has a health check access token' do
+ token = current_application_settings.health_check_access_token
+ expect(page).to have_content("Access token is #{token}")
+ expect(page).to have_selector('#health-check-token', text: token)
+ end
+
+ describe 'reload access token', js: true do
+ it 'changes the access token' do
+ orig_token = current_application_settings.health_check_access_token
+ click_button 'Reset health check access token'
+ wait_for_ajax
+ expect(find('#health-check-token').text).not_to eq orig_token
+ end
+ end
+ end
+
+ context 'when services are up' do
+ before do
+ visit admin_health_check_path
+ end
+
+ it 'shows healthy status' do
+ expect(page).to have_content('Current Status: Healthy')
+ end
+ end
+
+ context 'when a service is down' do
+ before do
+ allow(HealthCheck::Utils).to receive(:process_checks).and_return('The server is on fire')
+ visit admin_health_check_path
+ end
+
+ it 'shows unhealthy status' do
+ expect(page).to have_content('Current Status: Unhealthy')
+ expect(page).to have_content('The server is on fire')
+ end
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 7265cdac7a7..7964951ae99 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -6,15 +6,16 @@ describe "Admin::Hooks", feature: true do
login_as :admin
@system_hook = create(:system_hook)
-
end
describe "GET /admin/hooks" do
it "should be ok" do
visit admin_root_path
- page.within ".sidebar-wrapper" do
+
+ page.within ".layout-nav" do
click_on "Hooks"
end
+
expect(current_path).to eq(admin_hooks_path)
end
@@ -47,5 +48,4 @@ describe "Admin::Hooks", feature: true do
it { expect(current_path).to eq(admin_hooks_path) }
end
-
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 26d03944b8a..2f82fafc13a 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -8,8 +8,8 @@ describe "Admin Runners" do
describe "Runners page" do
before do
runner = FactoryGirl.create(:ci_runner)
- commit = FactoryGirl.create(:ci_commit)
- FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id)
+ pipeline = FactoryGirl.create(:ci_pipeline)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
visit admin_runners_path
end
@@ -60,6 +60,66 @@ describe "Admin Runners" do
it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).not_to have_content(@project2.name_with_namespace) }
end
+
+ describe 'enable/create' do
+ shared_examples 'assignable runner' do
+ it 'enables a runner for a project' do
+ within '.unassigned-projects' do
+ click_on 'Enable'
+ end
+
+ assigned_project = page.find('.assigned-projects')
+
+ expect(assigned_project).to have_content(@project2.path)
+ end
+ end
+
+ context 'with specific runner' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it_behaves_like 'assignable runner'
+ end
+
+ context 'with locked runner' do
+ before do
+ runner.update(locked: true)
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it_behaves_like 'assignable runner'
+ end
+
+ context 'with shared runner' do
+ before do
+ @project1.destroy
+ runner.update(is_shared: true)
+ visit admin_runner_path(runner)
+ end
+
+ it_behaves_like 'assignable runner'
+ end
+ end
+
+ describe 'disable/destroy' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it 'enables specific runner for project' do
+ within '.assigned-projects' do
+ click_on 'Disable'
+ end
+
+ new_runner_project = page.find('.unassigned-projects')
+
+ expect(new_runner_project).to have_content(@project1.path)
+ end
+ end
end
describe 'runners registration token' do
@@ -79,7 +139,7 @@ describe "Admin Runners" do
end
it 'changes registration token' do
- expect(page_token).to_not eq token
+ expect(page_token).not_to eq token
end
end
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
new file mode 100644
index 00000000000..f4e5c26b519
--- /dev/null
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe 'Admin System Info' do
+ before do
+ login_as :admin
+ end
+
+ describe 'GET /admin/system_info' do
+ it 'shows system info page' do
+ visit admin_system_info_path
+
+ expect(page).to have_content 'CPU'
+ expect(page).to have_content 'Memory'
+ expect(page).to have_content 'Disks'
+ end
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 4570e409128..767504df251 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication filters' do
it 'counts users who have enabled 2FA' do
- create(:user, two_factor_enabled: true)
+ create(:user, :two_factor)
visit admin_users_path
@@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have enabled 2FA' do
- user = create(:user, two_factor_enabled: true)
+ user = create(:user, :two_factor)
visit admin_users_path
click_link '2FA Enabled'
@@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do
end
it 'counts users who have not enabled 2FA' do
- create(:user, two_factor_enabled: false)
+ create(:user)
visit admin_users_path
@@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have not enabled 2FA' do
- user = create(:user, two_factor_enabled: false)
+ user = create(:user)
visit admin_users_path
click_link '2FA Disabled'
@@ -144,23 +144,19 @@ describe "Admin::Users", feature: true do
before { click_link 'Impersonate' }
it 'logs in as the user when impersonate is clicked' do
- page.within '.sidebar-user .username' do
- expect(page).to have_content(another_user.username)
- end
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
end
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
- expect(icon).to_not eql nil
+ expect(icon).not_to eql nil
end
it 'can log out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- page.within '.sidebar-user .username' do
- expect(page).to have_content(@user.username)
- end
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
@@ -173,7 +169,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
- @user.update_attribute(:two_factor_enabled, true)
+ @user.update_attribute(:otp_required_for_login, true)
visit admin_user_path(@user)
@@ -210,6 +206,8 @@ describe "Admin::Users", feature: true do
before do
fill_in "user_name", with: "Big Bang"
fill_in "user_email", with: "bigbang@mail.com"
+ fill_in "user_password", with: "AValidPassword1"
+ fill_in "user_password_confirmation", with: "AValidPassword1"
check "user_admin"
click_button "Save changes"
end
@@ -223,6 +221,7 @@ describe "Admin::Users", feature: true do
@simple_user.reload
expect(@simple_user.name).to eq('Big Bang')
expect(@simple_user.is_admin?).to be_truthy
+ expect(@simple_user.password_expires_at).to be <= Time.now
end
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index b710cb3c72f..4dd9548cfc5 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -5,8 +5,6 @@ describe "Dashboard Issues Feed", feature: true do
let!(:user) { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1) }
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
before do
project1.team << [user, :master]
@@ -14,16 +12,51 @@ describe "Dashboard Issues Feed", feature: true do
end
describe "atom feed" do
- it "should render atom feed via private token" do
+ it "renders atom feed via private token" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
- expect(response_headers['Content-Type']).
- to have_content('application/atom+xml')
+ expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
- expect(body).to have_selector('author email', text: issue1.author_email)
- expect(body).to have_selector('entry summary', text: issue1.title)
- expect(body).to have_selector('author email', text: issue2.author_email)
- expect(body).to have_selector('entry summary', text: issue2.title)
+ end
+
+ context "issue with basic fields" do
+ let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+
+ it "renders issue fields" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue2.author_email)
+ expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).not_to have_selector('labels')
+ expect(entry).not_to have_selector('milestone')
+ expect(entry).to have_selector('description', text: issue2.description)
+ end
+ end
+
+ context "issue with label and milestone" do
+ let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
+ let!(:label1) { create(:label, project: project1, title: 'label1') }
+ let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+
+ before do
+ issue1.labels << label1
+ end
+
+ it "renders issue label and milestone info" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue1.author_email)
+ expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('labels label', text: label1.title)
+ expect(entry).to have_selector('milestone', text: milestone1.title)
+ expect(entry).not_to have_selector('description')
+ end
end
end
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index de6aed74fb4..91704377a07 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -61,7 +61,7 @@ describe "User Feed", feature: true do
end
it 'should have XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
+ expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 090a941958f..16832c297ac 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -5,8 +5,9 @@ describe "Builds" do
before do
login_as(:user)
- @commit = FactoryGirl.create :ci_commit
- @build = FactoryGirl.create :ci_build, commit: @commit
+ @commit = FactoryGirl.create :ci_pipeline
+ @build = FactoryGirl.create :ci_build, pipeline: @commit
+ @build2 = FactoryGirl.create :ci_build
@project = @commit.project
@project.team << [@user, :developer]
end
@@ -46,7 +47,7 @@ describe "Builds" do
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' }
+ it { expect(page).not_to have_link 'Cancel running' }
end
end
@@ -62,17 +63,28 @@ describe "Builds" do
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' }
+ it { expect(page).not_to have_link 'Cancel running' }
end
describe "GET /:project/builds/:id" do
- before do
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ context "Build from project" do
+ before do
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ 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
- 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 }
+ context "Build from other project" do
+ before do
+ visit namespace_project_build_path(@project.namespace, @project, @build2)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
context "Download artifacts" do
before do
@@ -81,8 +93,42 @@ describe "Builds" do
end
it 'has button to download artifacts' do
- page.within('.artifacts') do
- expect(page).to have_content 'Download'
+ expect(page).to have_content 'Download'
+ end
+ end
+
+ context 'Artifacts expire date' do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ context 'no expire date defined' do
+ let(:expire_at) { nil }
+
+ it 'does not have the Keep button' do
+ expect(page).not_to have_content 'Keep'
+ end
+ end
+
+ context 'when expire date is defined' do
+ let(:expire_at) { Time.now + 7.days }
+
+ it 'keeps artifacts when Keep button is clicked' do
+ expect(page).to have_content 'The artifacts will be removed'
+ click_link 'Keep'
+
+ expect(page).not_to have_link 'Keep'
+ expect(page).not_to have_content 'The artifacts will be removed'
+ end
+ end
+
+ context 'when artifacts expired' do
+ let(:expire_at) { Time.now - 7.days }
+
+ it 'does not have the Keep button' do
+ expect(page).to have_content 'The artifacts were removed'
+ expect(page).not_to have_link 'Keep'
end
end
end
@@ -95,59 +141,144 @@ describe "Builds" do
end
it do
- page.within('.build-controls') do
- expect(page).to have_link 'Raw'
- end
+ expect(page).to have_link 'Raw'
end
end
end
describe "POST /:project/builds/:id/cancel" do
- before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link "Cancel"
+ context "Build from project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link "Cancel"
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content 'Retry' }
end
- it { expect(page).to have_content 'canceled' }
- it { expect(page).to have_content 'Retry' }
+ context "Build from other project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2))
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "POST /:project/builds/:id/retry" do
- before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link "Cancel"
- click_link 'Retry'
+ context "Build from project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link 'Cancel'
+ click_link 'Retry'
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ it { expect(page).to have_content 'pending' }
+ it { expect(page).to have_content 'Cancel' }
end
- it { expect(page).to have_content 'pending' }
- it { expect(page).to have_content 'Cancel' }
+ context "Build from other project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link 'Cancel'
+ page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "GET /:project/builds/:id/download" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.artifacts') { click_link 'Download' }
+ click_link 'Download'
end
- it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
+ context "Build from other project" do
+ before do
+ @build2.update_attributes(artifacts_file: artifacts_file)
+ visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "GET /:project/builds/:id/raw" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- @build.trace = 'BUILD TRACE'
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ context "Build from project" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
+ end
+
+ context "Build from other project" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build2.run!
+ @build2.trace = 'BUILD TRACE'
+ visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ puts page.status_code
+ puts current_url
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
+ end
+ end
+
+ describe "GET /:project/builds/:id/trace.json" do
+ context "Build from project" do
+ before do
+ visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json)
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ end
+
+ context "Build from other project" do
+ before do
+ visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
+ end
+
+ describe "GET /:project/builds/:id/status" do
+ context "Build from project" do
+ before do
+ visit status_namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it { expect(page.status_code).to eq(200) }
end
- it 'sends the right headers' do
- page.within('.build-controls') { click_link 'Raw' }
+ context "Build from other project" do
+ before do
+ visit status_namespace_project_build_path(@project.namespace, @project, @build2)
+ end
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ it { expect(page.status_code).to eq(404) }
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index dacaa96d760..45e1a157a1f 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -8,15 +8,15 @@ describe 'Commits' do
describe 'CI' do
before do
login_as :user
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
- let!(:commit) do
- FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha
+ let!(:pipeline) do
+ FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha
end
context 'commit status is Generic Commit Status' do
- let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit }
+ let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
before do
project.team << [@user, :reporter]
@@ -24,10 +24,10 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
- it { expect(page).to have_content commit.sha[0..7] }
+ it { expect(page).to have_content pipeline.sha[0..7] }
it 'contains generic commit status build' do
page.within('.table-holder') do
@@ -39,7 +39,7 @@ describe 'Commits' do
end
context 'commit status is Ci Build' do
- let!(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline }
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
context 'when logged as developer' do
@@ -53,7 +53,7 @@ describe 'Commits' do
end
it 'should show build status' do
- page.within("//li[@id='commit-#{commit.short_sha}']") do
+ page.within("//li[@id='commit-#{pipeline.short_sha}']") do
expect(page).to have_css(".ci-status-link")
end
end
@@ -61,12 +61,12 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
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 }
+ it { expect(page).to have_content pipeline.sha[0..7] }
+ it { expect(page).to have_content pipeline.git_commit_message }
+ it { expect(page).to have_content pipeline.git_author_name }
end
context 'Download artifacts' do
@@ -75,7 +75,7 @@ describe 'Commits' do
end
it do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Download artifacts'
expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
end
@@ -83,7 +83,7 @@ describe 'Commits' do
describe 'Cancel all builds' do
it 'cancels commit' do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
end
@@ -91,7 +91,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build' do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Cancel'
expect(page).to have_content 'canceled'
end
@@ -100,13 +100,13 @@ describe 'Commits' do
describe '.gitlab-ci.yml not found warning' do
context 'ci builds enabled' do
it "does not show warning" do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
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)
+ stub_ci_pipeline_yaml_file(nil)
+ visit ci_status_path(pipeline)
expect(page).to have_content '.gitlab-ci.yml not found in this commit'
end
end
@@ -114,8 +114,8 @@ describe 'Commits' do
context 'ci builds disabled' do
before do
stub_ci_builds_disabled
- stub_ci_commit_yaml_file(nil)
- visit ci_status_path(commit)
+ stub_ci_pipeline_yaml_file(nil)
+ visit ci_status_path(pipeline)
end
it 'does not show warning' do
@@ -129,16 +129,16 @@ describe 'Commits' do
before do
project.team << [@user, :reporter]
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
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_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message
+ expect(page).to have_content pipeline.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')
+ expect(page).not_to have_link('Cancel running')
+ expect(page).not_to have_link('Retry failed')
end
end
@@ -148,16 +148,16 @@ describe 'Commits' do
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
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')
+ expect(page).to have_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message
+ expect(page).to have_content pipeline.git_author_name
+ expect(page).not_to have_link('Download artifacts')
+ expect(page).not_to have_link('Cancel running')
+ expect(page).not_to have_link('Retry failed')
end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
new file mode 100644
index 00000000000..203e55a36f2
--- /dev/null
+++ b/spec/features/container_registry_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe "Container Registry" do
+ let(:project) { create(:empty_project) }
+ let(:repository) { project.container_registry_repository }
+ let(:tag_name) { 'latest' }
+ let(:tags) { [tag_name] }
+
+ before do
+ login_as(:user)
+ project.team << [@user, :developer]
+ stub_container_registry_tags(*tags)
+ stub_container_registry_config(enabled: true)
+ allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ end
+
+ describe 'GET /:project/container_registry' do
+ before do
+ visit namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
+ context 'when no tags' do
+ let(:tags) { [] }
+
+ it { expect(page).to have_content('No images in Container Registry for this project') }
+ end
+
+ context 'when there are tags' do
+ it { expect(page).to have_content(tag_name) }
+ it { expect(page).to have_content('d7a513a66') }
+ end
+ end
+
+ describe 'DELETE /:project/container_registry/tag' do
+ before do
+ visit namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
+ it do
+ expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
+
+ click_on 'Remove'
+ end
+ end
+end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
new file mode 100644
index 00000000000..365cb445df1
--- /dev/null
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Tooltips on .timeago dates', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:created_date) { Date.yesterday.to_time }
+ let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') }
+
+ context 'on the activity tab' do
+ before do
+ project.team << [user, :master]
+
+ Event.create( project: project, author_id: user.id, action: Event::JOINED,
+ updated_at: created_date, created_at: created_date)
+
+ login_as user
+ visit user_path(user)
+ wait_for_ajax()
+
+ page.find('.js-timeago').hover
+ end
+
+ it 'has the datetime formated correctly' do
+ expect(page).to have_selector('.local-timeago', text: expected_format)
+ end
+ end
+
+ context 'on the snippets tab' do
+ before do
+ project.team << [user, :master]
+ create(:snippet, author: user, updated_at: created_date, created_at: created_date)
+
+ login_as user
+ visit user_snippets_path(user)
+ wait_for_ajax()
+
+ page.find('.js-timeago').hover
+ end
+
+ it 'has the datetime formated correctly' do
+ expect(page).to have_selector('.local-timeago', text: expected_format)
+ end
+ end
+end
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index cf86e2c85e9..c2e0612aef8 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe "Dashboard > User filters projects", feature: true do
-
describe 'filtering personal projects' do
before do
user = create(:user)
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
new file mode 100644
index 00000000000..7fb28f4174b
--- /dev/null
+++ b/spec/features/environments_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+feature 'Environments', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when showing environments' do
+ given!(:environment) { }
+ given!(:deployment) { }
+
+ before do
+ visit namespace_project_environments_path(project.namespace, project)
+ end
+
+ context 'without environments' do
+ scenario 'does show no environments' do
+ expect(page).to have_content('You don\'t have any environments right now.')
+ end
+ end
+
+ context 'with environments' do
+ given(:environment) { create(:environment, project: project) }
+
+ scenario 'does show environment name' do
+ expect(page).to have_link(environment.name)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('No deployments yet')
+ end
+ end
+
+ context 'with deployments' do
+ given(:deployment) { create(:deployment, environment: environment) }
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+ end
+ end
+
+ scenario 'does have a New environment button' do
+ expect(page).to have_link('New environment')
+ end
+ end
+
+ describe 'when showing the environment' do
+ given(:environment) { create(:environment, project: project) }
+ given!(:deployment) { }
+
+ before do
+ visit namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'with deployments' do
+ given(:deployment) { create(:deployment, environment: environment) }
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+
+ scenario 'does not show a retry button for deployment without build' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ context 'with build' do
+ given(:build) { create(:ci_build, project: project) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show build name' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ scenario 'does show retry button' do
+ expect(page).to have_link('Retry')
+ end
+ end
+ end
+ end
+
+ describe 'when creating a new environment' do
+ before do
+ visit namespace_project_environments_path(project.namespace, project)
+ end
+
+ context 'when logged as developer' do
+ before do
+ click_link 'New environment'
+ end
+
+ context 'for valid name' do
+ before do
+ fill_in('Name', with: 'production')
+ click_on 'Create environment'
+ end
+
+ scenario 'does create a new pipeline' do
+ expect(page).to have_content('Production')
+ end
+ end
+
+ context 'for invalid name' do
+ before do
+ fill_in('Name', with: 'name with spaces')
+ click_on 'Create environment'
+ end
+
+ scenario 'does show errors' do
+ expect(page).to have_content('Name can contain only letters')
+ end
+ end
+ end
+
+ context 'when logged as reporter' do
+ given(:role) { :reporter }
+
+ scenario 'does not have a New environment link' do
+ expect(page).not_to have_link('New environment')
+ end
+ end
+ end
+
+ describe 'when deleting existing environment' do
+ given(:environment) { create(:environment, project: project) }
+
+ before do
+ visit namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ context 'when logged as master' do
+ given(:role) { :master }
+
+ scenario 'does delete environment' do
+ click_link 'Destroy'
+ expect(page).not_to have_link(environment.name)
+ end
+ end
+
+ context 'when logged as developer' do
+ given(:role) { :developer }
+
+ scenario 'does not have a Destroy link' do
+ expect(page).not_to have_link('Destroy')
+ end
+ end
+ end
+end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 7852c39fee2..a89ac09f236 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -81,7 +81,6 @@ describe "GitLab Flavored Markdown", feature: true do
end
end
-
describe "for merge requests" do
before do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
@@ -100,7 +99,6 @@ describe "GitLab Flavored Markdown", feature: true do
end
end
-
describe "for milestones" do
before do
@milestone = create(:milestone,
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
new file mode 100644
index 00000000000..33bf6d3752f
--- /dev/null
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Last owner cannot leave group', feature: true do
+ let(:owner) { create(:user) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(owner)
+ visit group_path(group)
+ end
+
+ scenario 'user does not see a "Leave Group" link' do
+ expect(page).not_to have_content 'Leave Group'
+ end
+end
diff --git a/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb
new file mode 100644
index 00000000000..37c433cc09a
--- /dev/null
+++ b/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Member cannot request access to his project', feature: true do
+ let(:member) { create(:user) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_developer(member)
+ login_as(member)
+ visit group_path(group)
+ end
+
+ scenario 'member does not see the request access button' do
+ expect(page).not_to have_content 'Request Access'
+ end
+end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
new file mode 100644
index 00000000000..3185ff924b9
--- /dev/null
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Member leaves group', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.add_owner(owner)
+ group.add_developer(user)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user leaves group' do
+ click_link 'Leave Group'
+
+ expect(current_path).to eq(dashboard_groups_path)
+ expect(group.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..10d3713f19f
--- /dev/null
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Owner manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.request_access(user)
+ group.add_owner(owner)
+ login_as(owner)
+ end
+
+ scenario 'owner can see access requests' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+ end
+
+ scenario 'master can grant access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
+ end
+
+ def expect_visible_access_request(group, user)
+ expect(group.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{group.name} access requests 1"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..d1a6a98ab72
--- /dev/null
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+feature 'Groups > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :private, namespace: group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user can request access to a group' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Request to join the #{group.name} group"
+
+ expect(group.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Group'
+ end
+
+ scenario 'user does not see private projects' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(page).not_to have_content project.name
+ end
+
+ scenario 'user does not see group in the Dashboard > Groups page' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ visit dashboard_groups_path
+
+ expect(page).not_to have_content group.name
+ end
+
+ scenario 'user is not listed in the group members page' do
+ click_link 'Request Access'
+
+ expect(group.requesters.exists?(user_id: user)).to be_truthy
+
+ click_link 'Members'
+
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(group.requesters.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(group.requesters.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the group has been withdrawn.'
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 41af789aae2..07a854ea014 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do
end
context 'click the thumbsup emoji' do
-
it 'should increment the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
sleep 2
@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do
end
context 'click the thumbsdown emoji' do
-
it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
new file mode 100644
index 00000000000..63efecf8780
--- /dev/null
+++ b/spec/features/issues/award_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Issue awards', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ describe 'logged in' do
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should add award to issue' do
+ first('.js-emoji-btn').click
+ expect(page).to have_selector('.js-emoji-btn.active')
+ expect(first('.js-emoji-btn')).to have_content '1'
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(first('.js-emoji-btn')).to have_content '1'
+ end
+
+ it 'should remove award from issue' do
+ first('.js-emoji-btn').click
+ find('.js-emoji-btn.active').click
+ expect(first('.js-emoji-btn')).to have_content '0'
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(first('.js-emoji-btn')).to have_content '0'
+ end
+
+ it 'should only have one menu on the page' do
+ first('.js-add-award').click
+ expect(page).to have_selector('.emoji-menu')
+
+ expect(page).to have_selector('.emoji-menu', count: 1)
+ end
+ end
+
+ describe 'logged out' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
+end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
new file mode 100644
index 00000000000..afc093cc1f5
--- /dev/null
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -0,0 +1,359 @@
+require 'rails_helper'
+
+feature 'Issues > Labels bulk assignment', feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
+ let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:label, project: project, title: 'feature') }
+
+ context 'as an allowed user', js: true do
+ before do
+ project.team << [user, :master]
+
+ login_as user
+ end
+
+ context 'can bulk assign' do
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'a label' do
+ context 'to all issues' do
+ before do
+ check 'check_all_issues'
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ end
+ end
+
+ context 'to a issue' do
+ before do
+ check "selected_issue_#{issue1.id}"
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ end
+ end
+ end
+
+ context 'multiple labels' do
+ context 'to all issues' do
+ before do
+ check 'check_all_issues'
+ open_labels_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+
+ context 'to a issue' do
+ before do
+ check "selected_issue_#{issue1.id}"
+ open_labels_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ end
+ end
+ end
+ end
+
+ context 'can assign a label to all issues when label is present' do
+ before do
+ issue2.labels << bug
+ issue2.labels << feature
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check 'check_all_issues'
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ end
+ end
+
+ context 'can bulk un-assign' do
+ context 'all labels to all issues' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check 'check_all_issues'
+ unmark_labels_in_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ end
+ end
+
+ context 'a label to a issue' do
+ before do
+ issue1.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check_issue issue1
+ unmark_labels_in_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+
+ context 'a label and keep the others label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check_issue issue1
+ check_issue issue2
+ unmark_labels_in_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+ end
+
+ context 'toggling a milestone' do
+ let!(:milestone) { create(:milestone, project: project, title: 'First Release') }
+
+ context 'setting a milestone' do
+ before do
+ issue1.labels << bug
+ issue2.labels << feature
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'labels are kept' do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+
+ check 'check_all_issues'
+ open_milestone_dropdown(['First Release'])
+ update_issues
+
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
+ end
+ end
+
+ context 'setting a milestone and adding another label' do
+ before do
+ issue1.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'existing label is kept and new label is present' do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+
+ check 'check_all_issues'
+ open_milestone_dropdown ['First Release']
+ open_labels_dropdown ['feature']
+ update_issues
+
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
+ end
+ end
+
+ context 'setting a milestone and removing existing label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'existing label is kept and new label is present' do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+
+ check 'check_all_issues'
+ open_milestone_dropdown ['First Release']
+ unmark_labels_in_dropdown ['feature']
+ update_issues
+
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
+ end
+ end
+
+ context 'unsetting a milestone' do
+ before do
+ issue1.milestone = milestone
+ issue2.milestone = milestone
+ issue1.save
+ issue2.save
+ issue1.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'labels are kept' do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
+
+ check 'check_all_issues'
+ open_milestone_dropdown(['No Milestone'])
+ update_issues
+
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'First Release'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'First Release'
+ end
+ end
+ end
+
+ context 'toggling checked issues' do
+ before do
+ issue1.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+
+ check_issue issue1
+ open_labels_dropdown ['feature']
+ uncheck_issue issue1
+ check_issue issue1
+ update_issues
+ sleep 1 # needed
+
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
+ end
+ end
+ end
+
+ context 'as a guest' do
+ before do
+ login_as user
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'cannot bulk assign labels' do
+ it do
+ expect(page).not_to have_css '.check_all_issues'
+ expect(page).not_to have_css '.issue-check'
+ end
+ end
+ end
+
+ def open_milestone_dropdown(items = [])
+ page.within('.issues_bulk_update') do
+ click_button 'Milestone'
+ wait_for_ajax
+ items.map do |item|
+ click_link item
+ end
+ end
+ end
+
+ def open_labels_dropdown(items = [], unmark = false)
+ page.within('.issues_bulk_update') do
+ click_button 'Label'
+ wait_for_ajax
+ items.map do |item|
+ click_link item
+ end
+ if unmark
+ items.map do |item|
+ # Make sure we are unmarking the item no matter the state it has currently
+ click_link item until find('a', text: item)[:class] == 'label-item'
+ end
+ end
+ end
+ end
+
+ def unmark_labels_in_dropdown(items = [])
+ open_labels_dropdown(items, true)
+ end
+
+ def check_issue(issue, uncheck = false)
+ page.within('.issues-list') do
+ if uncheck
+ uncheck "selected_issue_#{issue.id}"
+ else
+ check "selected_issue_#{issue.id}"
+ end
+ end
+ end
+
+ def uncheck_issue(issue)
+ check_issue(issue, true)
+ end
+
+ def update_issues
+ click_button 'Update issues'
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 7f654684143..5ea02b8d39c 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -54,6 +54,12 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
+
+ it 'should remove label "bug"' do
+ find('.js-label-filter-remove').click
+ wait_for_ajax
+ expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
+ end
end
context 'filter by label feature', js: true do
@@ -135,6 +141,12 @@ feature 'Issue filtering by Labels', feature: true do
it 'should not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
+
+ it 'should remove label "enhancement"' do
+ find('.js-label-filter-remove', match: :first).click
+ wait_for_ajax
+ expect(find('.filtered-labels')).to have_no_content "enhancement"
+ end
end
context 'filter by label enhancement and bug in issues list', js: true do
@@ -164,4 +176,42 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
+
+ context 'remove filtered labels', js: true do
+ before do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ click_link 'bug'
+ find('.dropdown-menu-close').click
+ end
+
+ page.within '.filtered-labels' do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ it 'should allow user to remove filtered labels' do
+ first('.js-label-filter-remove').click
+ wait_for_ajax
+
+ expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
+ expect(find('.labels-filter')).not_to have_content 'bug'
+ end
+ end
+
+ context 'dropdown filtering', js: true do
+ it 'should filter by label name' do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ fill_in 'label-name', with: 'bug'
+
+ page.within '.dropdown-content' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 192e3619375..4b9b5394b61 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -1,11 +1,13 @@
require 'rails_helper'
describe 'Filter issues', feature: true do
+ include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
+ let!(:issue1) { create(:issue, project: project) }
before do
project.team << [user, :master]
@@ -13,7 +15,6 @@ describe 'Filter issues', feature: true do
end
describe 'Filter issues for assignee from issues#index' do
-
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -21,7 +22,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
end
context 'assignee', js: true do
@@ -35,7 +36,6 @@ describe 'Filter issues', feature: true do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
-
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
@@ -45,7 +45,6 @@ describe 'Filter issues', feature: true do
end
describe 'Filter issues for milestone from issues#index' do
-
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -53,7 +52,7 @@ describe 'Filter issues', feature: true do
find('.milestone-filter .dropdown-content a', text: milestone.title).click
- sleep 2
+ wait_for_ajax
end
context 'milestone', js: true do
@@ -67,7 +66,6 @@ describe 'Filter issues', feature: true do
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
-
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
@@ -80,23 +78,21 @@ describe 'Filter issues', feature: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
+ wait_for_ajax
end
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
- page.within '.labels-filter' do
- expect(page).to have_content 'Any Label'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
+ expect(find('.labels-filter')).to have_content 'Label'
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
@@ -114,7 +110,6 @@ describe 'Filter issues', feature: true do
end
describe 'Filter issues for assignee and label from issues#index' do
-
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -122,14 +117,14 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
end
context 'assignee and label', js: true do
@@ -145,7 +140,6 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
-
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
@@ -154,4 +148,149 @@ describe 'Filter issues', feature: true do
end
end
end
+
+ describe 'filter issues by text' do
+ before do
+ create(:issue, title: "Bug", project: project)
+
+ bug_label = create(:label, project: project, title: 'bug')
+ milestone = create(:milestone, title: "8", project: project)
+
+ issue = create(:issue,
+ title: "Bug 2",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue.labels << bug_label
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'only text', js: true do
+ it 'should filter issues by searched text' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+ end
+
+ it 'should not show any issues' do
+ fill_in 'issue_search', with: 'testing'
+
+ page.within '.issues-list' do
+ expect(page).not_to have_selector('.issue')
+ end
+ end
+ end
+
+ context 'text and dropdown options', js: true do
+ it 'should filter by text and label' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Label'
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+ find('.dropdown-menu-close-icon').click
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and milestone' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Milestone'
+ page.within '.milestone-filter' do
+ click_link '8'
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and assignee' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Assignee'
+ page.within '.dropdown-menu-assignee' do
+ click_link user.name
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and author' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Author'
+ page.within '.dropdown-menu-author' do
+ click_link user.name
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+ end
+ end
+
+ describe 'filter issues and sort', js: true do
+ before do
+ bug_label = create(:label, project: project, title: 'bug')
+ bug_one = create(:issue, title: "Frontend", project: project)
+ bug_two = create(:issue, title: "Bug 2", project: project)
+
+ bug_one.labels << bug_label
+ bug_two.labels << bug_label
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'should be able to filter and sort issues' do
+ click_button 'Label'
+ wait_for_ajax
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+ find('.dropdown-menu-close-icon').click
+ wait_for_ajax
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Last created'
+ page.within '.dropdown-menu-sort' do
+ click_link 'Oldest created'
+ end
+ wait_for_ajax
+
+ page.within '.issues-list' do
+ expect(page).to have_content('Frontend')
+ end
+ end
+ end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 84c8e20ebaa..7773c486b4e 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -19,13 +19,14 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project not allowed' do
- expect(page).to have_no_select('move_to_project_id')
+ expect(page).to have_no_selector('#move_to_project_id')
end
end
context 'user has permission to move issue' do
let!(:mr) { create(:merge_request, source_project: old_project) }
let(:new_project) { create(:project) }
+ let(:new_project_search) { create(:project) }
let(:text) { 'Text with !1' }
let(:cross_reference) { old_project.to_reference }
@@ -37,7 +38,7 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project' do
- select(new_project.name_with_namespace, from: 'move_to_project_id')
+ first('#move_to_project_id', visible: false).set(new_project.id)
click_button('Save changes')
expect(current_url).to include project_path(new_project)
@@ -47,14 +48,33 @@ feature 'issue move to another project' do
expect(page).to have_content(issue.title)
end
- context 'projects user does not have permission to move issue to exist' do
+ scenario 'searching project dropdown', js: true do
+ new_project_search.team << [user, :reporter]
+
+ page.within '.js-move-dropdown' do
+ first('.select2-choice').click
+ end
+
+ fill_in('s2id_autogen2_search', with: new_project_search.name)
+
+ page.within '.select2-drop' do
+ expect(page).to have_content(new_project_search.name)
+ expect(page).not_to have_content(new_project.name)
+ end
+ end
+
+ context 'user does not have permission to move the issue to a project', js: true do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- options = [ '', 'No project', new_project.name_with_namespace ]
- expect(page).to have_select('move_to_project_id', options: options)
+ click_link 'Select project'
+
+ page.within '.select2-results' do
+ expect(page).to have_content 'No project'
+ expect(page).to have_content new_project.name_with_namespace
+ end
end
end
@@ -65,7 +85,7 @@ feature 'issue move to another project' do
end
scenario 'user wants to move issue that has already been moved' do
- expect(page).to have_no_select('move_to_project_id')
+ expect(page).to have_no_selector('#move_to_project_id')
end
end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index e4efdbe2421..f5cfe2d666e 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -9,8 +9,11 @@ feature 'Issue notes polling' do
end
scenario 'Another user adds a comment to an issue', js: true do
- note = create(:note_on_issue, noteable: issue, note: 'Looks good!')
+ note = create(:note, noteable: issue, project: project,
+ note: 'Looks good!')
+
page.execute_script('notes.refresh();')
+
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
end
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
new file mode 100644
index 00000000000..bc0f437a8ce
--- /dev/null
+++ b/spec/features/issues/todo_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+feature 'Manually create a todo item from issue', feature: true, js: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should create todo when clicking button' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ expect(page).to have_content 'Mark Done'
+ end
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
+ end
+
+ it 'should mark a todo as done' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ click_button 'Mark Done'
+ end
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
+ end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index b03dd0f666d..ddbd69b2891 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do
+ include WaitForAjax
+
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'should be set to open' do
create_closed
- visit namespace_project_issues_path(project.namespace, project)
-
- find('.issues-state-filters a', text: 'Closed').click
+ visit namespace_project_issues_path(project.namespace, project, state: 'closed')
find('#check_all_issues').click
find('.js-issue-status').click
@@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
- find('.js-update-assignee').click
+ click_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click
click_update_issues_button
@@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
- find('.js-update-assignee').click
+ click_update_assignee_button
click_link 'Unassigned'
click_update_issues_button
-
- within first('.issue .controls') do
- expect(page).to have_no_selector('.author_link')
- end
+ expect(find('.issue:first-child .controls')).not_to have_css('.author_link')
end
end
@@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button
- expect(first('.issue')).to_not have_content milestone.title
+ expect(find('.issue:first-child')).not_to have_content milestone.title
end
end
@@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do
create(:issue, project: project, milestone: milestone)
end
+ def click_update_assignee_button
+ find('.js-update-assignee').click
+ wait_for_ajax
+ end
+
def click_update_issues_button
find('.update_selected_issues').click
+ wait_for_ajax
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d5755c293c5..d51c9abea19 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- click_button "Go full screen"
+ find('.js-zen-enter').click
end
it 'should open new issue popup' do
@@ -64,10 +64,70 @@ describe 'Issues', feature: true do
end
end
+ describe 'due date', js: true do
+ context 'on new form' do
+ before do
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'should save with due date' do
+ date = Date.today.at_beginning_of_month
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.ui-datepicker' do
+ click_link date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+
+ context 'on edit form' do
+ let(:issue) { create(:issue, author: @user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
+
+ before do
+ visit edit_namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should save with due date' do
+ date = Date.today.at_beginning_of_month
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ date = date.tomorrow
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.ui-datepicker' do
+ click_link date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+ end
+
describe 'Issue info' do
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
- create(:upvote_note, noteable: issue)
+ create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
@@ -301,19 +361,14 @@ describe 'Issues', feature: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
context 'by authorized user' do
-
it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
page.within('.assignee') do
expect(page).to have_content "#{@user.name}"
- end
- find('.block.assignee .edit-link').click
- sleep 2 # wait for ajax stuff to complete
- first('.dropdown-menu-user-link').click
- sleep 2
- page.within('.assignee') do
+ click_link 'Edit'
+ click_link 'Unassigned'
expect(page).to have_content 'No assignee'
end
@@ -331,7 +386,7 @@ describe 'Issues', feature: true do
page.within '.assignee' do
click_link 'Edit'
end
-
+
page.within '.dropdown-menu-user' do
click_link @user.name
end
@@ -340,10 +395,30 @@ describe 'Issues', feature: true do
expect(page).to have_content @user.name
end
end
+
+ it 'allows user to unselect themselves', js: true do
+ issue2 = create(:issue, project: project, author: @user)
+ visit namespace_project_issue_path(project.namespace, project, issue2)
+
+ page.within '.assignee' do
+ click_link 'Edit'
+ click_link @user.name
+
+ page.within '.value' do
+ expect(page).to have_content @user.name
+ end
+
+ click_link 'Edit'
+ click_link @user.name
+
+ page.within '.value' do
+ expect(page).to have_content "No assignee"
+ end
+ end
+ end
end
context 'by unauthorized user' do
-
let(:guest) { create(:user) }
before do
@@ -365,8 +440,6 @@ describe 'Issues', feature: true do
let!(:milestone) { create(:milestone, project: project) }
context 'by authorized user' do
-
-
it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
@@ -384,6 +457,26 @@ describe 'Issues', feature: true do
expect(issue.reload.milestone).to be_nil
end
+
+ it 'allows user to de-select milestone', js: true do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ page.within('.milestone') do
+ click_link 'Edit'
+ click_link milestone.title
+
+ page.within '.value' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_link 'Edit'
+ click_link milestone.title
+
+ page.within '.value' do
+ expect(page).to have_content 'None'
+ end
+ end
+ end
end
context 'by unauthorized user' do
@@ -431,6 +524,43 @@ describe 'Issues', feature: true do
end
end
+ describe 'due date' do
+ context 'update due on issue#show', js: true do
+ let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should add due date to issue' do
+ page.within '.due_date' do
+ click_link 'Edit'
+
+ page.within '.ui-datepicker-calendar' do
+ first('.ui-state-default').click
+ end
+
+ expect(page).to have_no_content 'None'
+ end
+ end
+
+ it 'should remove due date from issue' do
+ page.within '.due_date' do
+ click_link 'Edit'
+
+ page.within '.ui-datepicker-calendar' do
+ first('.ui-state-default').click
+ end
+
+ expect(page).to have_no_content 'No due date'
+
+ click_link 'remove due date'
+ expect(page).to have_content 'No due date'
+ end
+ end
+ end
+ end
+
def first_issue
page.all('ul.issues-list > li').first.text
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 8c38dd5b122..72b5ff231f7 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -32,12 +32,12 @@ feature 'Login', feature: true do
let(:user) { create(:user, :two_factor) }
before do
- login_with(user)
- expect(page).to have_content('Two-factor Authentication')
+ login_with(user, remember: true)
+ expect(page).to have_content('Two-Factor Authentication')
end
def enter_code(code)
- fill_in 'Two-factor Authentication code', with: code
+ fill_in 'Two-Factor Authentication code', with: code
click_button 'Verify code'
end
@@ -52,6 +52,12 @@ feature 'Login', feature: true do
expect(current_path).to eq root_path
end
+ it 'persists remember_me value via hidden field' do
+ field = first('input#user_remember_me', visible: false)
+
+ expect(field.value).to eq '1'
+ end
+
it 'blocks login with invalid code' do
enter_code('foo')
expect(page).to have_content('Invalid two-factor code')
@@ -121,7 +127,7 @@ feature 'Login', feature: true do
user = create(:user, password: 'not-the-default')
login_with(user)
- expect(page).to have_content('Invalid login or password.')
+ expect(page).to have_content('Invalid Login or password.')
end
end
@@ -137,12 +143,12 @@ 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 enable Two-factor Authentication for your account before')
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content('You must enable Two-Factor Authentication for your account before')
end
- it 'disallows skipping two-factor configuration' do
- expect(current_path).to eq new_profile_two_factor_auth_path
+ it 'allows skipping two-factor configuration', js: true do
+ expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
@@ -153,26 +159,26 @@ feature 'Login', feature: true do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
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 enable Two-factor Authentication for your account.')
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
end
- it 'disallows skipping two-factor configuration' do
- expect(current_path).to eq new_profile_two_factor_auth_path
+ it 'disallows skipping two-factor configuration', js: true do
+ expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
- context 'without grace pariod defined' do
+ context 'without grace period defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 0)
login_with(user)
end
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 enable Two-factor Authentication for your account.')
+ expect(current_path).to eq profile_two_factor_auth_path
+ 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 0148c87084a..09ccc77c101 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -165,22 +165,32 @@ describe 'GitLab Markdown', feature: true do
describe 'ExternalLinkFilter' do
it 'adds nofollow to external link' do
link = doc.at_css('a:contains("Google")')
+
expect(link.attr('rel')).to include('nofollow')
end
it 'adds noreferrer to external link' do
link = doc.at_css('a:contains("Google")')
+
expect(link.attr('rel')).to include('noreferrer')
end
+ it 'adds _blank to target attribute for external links' do
+ link = doc.at_css('a:contains("Google")')
+
+ expect(link.attr('target')).to match('_blank')
+ end
+
it 'ignores internal link' do
link = doc.at_css('a:contains("GitLab Root")')
+
expect(link.attr('rel')).not_to match 'nofollow'
+ expect(link.attr('target')).not_to match '_blank'
end
end
end
- before(:all) do
+ before do
@feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable
@@ -188,7 +198,7 @@ describe 'GitLab Markdown', feature: true do
end
context 'default pipeline' do
- before(:all) do
+ before do
@html = markdown(@feat.raw_markdown)
end
@@ -231,13 +241,14 @@ describe 'GitLab Markdown', feature: true do
context 'wiki pipeline' do
before do
@project_wiki = @feat.project_wiki
+ @project_wiki_page = @feat.project_wiki_page
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)
allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
- @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
+ @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug })
end
it_behaves_like 'all pipelines'
@@ -278,6 +289,10 @@ describe 'GitLab Markdown', feature: true do
it 'includes GollumTagsFilter' do
expect(doc).to parse_gollum_tags
end
+
+ it 'includes InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
end
# Fake a `current_user` helper
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
new file mode 100644
index 00000000000..007f67d6080
--- /dev/null
+++ b/spec/features/merge_requests/award_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Merge request awards', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ describe 'logged in' do
+ before do
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should add award to merge request' do
+ first('.js-emoji-btn').click
+ expect(page).to have_selector('.js-emoji-btn.active')
+ expect(first('.js-emoji-btn')).to have_content '1'
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ expect(first('.js-emoji-btn')).to have_content '1'
+ end
+
+ it 'should remove award from merge request' do
+ first('.js-emoji-btn').click
+ find('.js-emoji-btn.active').click
+ expect(first('.js-emoji-btn')).to have_content '0'
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ expect(first('.js-emoji-btn')).to have_content '0'
+ end
+
+ it 'should only have one menu on the page' do
+ first('.js-add-award').click
+ expect(page).to have_selector('.emoji-menu')
+
+ expect(page).to have_selector('.emoji-menu', count: 1)
+ end
+ end
+
+ describe 'logged out' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
new file mode 100644
index 00000000000..f676200ecf3
--- /dev/null
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+feature 'Merge request created from fork' do
+ given(:user) { create(:user) }
+ given(:project) { create(:project, :public) }
+ given(:fork_project) { create(:project, :public) }
+
+ given!(:merge_request) do
+ create(:forked_project_link, forked_to_project: fork_project,
+ forked_from_project: project)
+
+ create(:merge_request_with_diffs, source_project: fork_project,
+ target_project: project,
+ description: 'Test merge request')
+ end
+
+ background do
+ fork_project.team << [user, :master]
+ login_as user
+ end
+
+ scenario 'user can access merge request' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_content 'Test merge request'
+ end
+
+ context 'pipeline present in source project' do
+ include WaitForAjax
+
+ given(:pipeline) do
+ create(:ci_pipeline_with_two_job, project: fork_project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ end
+
+ background { pipeline.create_builds(user) }
+
+ scenario 'user visits a pipelines page', js: true do
+ visit_merge_request(merge_request)
+ page.within('.merge-request-tabs') { click_link 'Builds' }
+ wait_for_ajax
+
+ page.within('table.builds') do
+ expect(page).to have_content 'rspec'
+ expect(page).to have_content 'spinach'
+ end
+
+ expect(find_link('Cancel running')[:href])
+ .to include fork_project.path_with_namespace
+ end
+ end
+
+ def visit_merge_request(mr)
+ visit namespace_project_merge_request_path(project.namespace,
+ project, mr)
+ end
+end
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index 7aa7eb965e9..96f7b8c9932 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -12,8 +12,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
context "Active build for Merge Request" do
- let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, commit: ci_commit) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
+ let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
before do
login_as user
@@ -47,8 +47,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
end
- let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, commit: ci_commit) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
+ let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
before do
login_as user
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
new file mode 100644
index 00000000000..80e8b8fc642
--- /dev/null
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project) }
+
+ before do
+ login_as merge_request.author
+
+ project.team << [merge_request.author, :master]
+ end
+
+ context 'project does not have CI enabled' do
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when project has CI enabled' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) }
+
+ context 'when merge requests can only be merged if the build succeeds' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'does not allow to merge immediately' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+ expect(page).not_to have_button 'Select Merge Moment'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'does not allow MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).not_to have_button 'Accept Merge Request'
+ expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+
+ context 'when merge requests can be merged when the build failed' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'allows MR to be merged immediately', js: true do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+
+ click_button 'Select Merge Moment'
+ expect(page).to have_content 'Merge Immediately'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+ end
+
+ def visit_merge_request(merge_request)
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+end
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index cc7f78e7325..1c130057c56 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -38,6 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
expect(page).to have_content 'lfs'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
+ expect(count_merge_requests).to eq(1)
end
it 'filters on a specific assignee' do
@@ -46,6 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
expect(page).not_to have_content 'lfs'
expect(page).to have_content 'fix'
expect(page).to have_content 'markdown'
+ expect(count_merge_requests).to eq(2)
end
it 'sorts by newest' do
@@ -53,6 +55,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
expect(first_merge_request).to include('lfs')
expect(last_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(3)
end
it 'sorts by oldest' do
@@ -60,30 +63,35 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
expect(first_merge_request).to include('fix')
expect(last_merge_request).to include('lfs')
+ expect(count_merge_requests).to eq(3)
end
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
expect(first_merge_request).to include('lfs')
+ expect(count_merge_requests).to eq(3)
end
it 'sorts by oldest updated' do
visit_merge_requests(project, sort: sort_value_oldest_updated)
expect(first_merge_request).to include('markdown')
+ expect(count_merge_requests).to eq(3)
end
it 'sorts by milestone due soon' do
visit_merge_requests(project, sort: sort_value_milestone_soon)
expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(3)
end
it 'sorts by milestone due later' do
visit_merge_requests(project, sort: sort_value_milestone_later)
expect(first_merge_request).to include('markdown')
+ expect(count_merge_requests).to eq(3)
end
it 'filters on one label and sorts by due soon' do
@@ -94,6 +102,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
sort: sort_value_due_date_soon)
expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(1)
end
context 'while filtering on two labels' do
@@ -110,6 +119,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
sort: sort_value_due_date_soon)
expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(1)
end
context 'filter on assignee and' do
@@ -119,6 +129,16 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
sort: sort_value_due_date_soon)
expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(1)
+ end
+
+ it 'sorts by recently due milestone' do
+ visit namespace_project_merge_requests_path(project.namespace, project,
+ label_name: [label.name, label2.name],
+ assignee_id: user.id,
+ sort: sort_value_milestone_soon)
+
+ expect(first_merge_request).to include('fix')
end
end
end
@@ -134,4 +154,8 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
def last_merge_request
page.all('ul.mr-list > li').last.text
end
+
+ def count_merge_requests
+ page.all('ul.mr-list > li').count
+ end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 389812ff7e1..5174168713c 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -4,25 +4,15 @@ describe 'Comments', feature: true do
include RepoHelpers
include WaitForAjax
- describe 'On merge requests page', feature: true do
- it 'excludes award_emoji from comment count' do
- merge_request = create(:merge_request)
- project = merge_request.source_project
- create(:upvote_note, noteable: merge_request, project: project)
-
- login_as :admin
- visit namespace_project_merge_requests_path(project.namespace, project)
-
- expect(merge_request.mr_and_commit_notes.count).to eq 1
- expect(page.all('.merge-request-no-comments').first.text).to eq "0"
+ describe 'On a merge request', js: true, feature: true do
+ let!(:project) { create(:project) }
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
end
- end
- describe 'On a merge request', js: true, feature: true do
- let!(:merge_request) { create(:merge_request) }
- let!(:project) { merge_request.source_project }
let!(:note) do
- create(:note_on_merge_request, :with_attachment, project: project)
+ create(:note_on_merge_request, :with_attachment, noteable: merge_request,
+ project: project)
end
before do
@@ -143,17 +133,6 @@ describe 'Comments', feature: true do
end
end
end
-
- describe 'comment info' do
- it 'excludes award_emoji from comment count' do
- create(:upvote_note, noteable: merge_request, project: project)
-
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
-
- expect(merge_request.mr_and_commit_notes.count).to eq 2
- expect(find('.notes-tab span.badge').text).to eq "1"
- end
- end
end
describe 'On a merge request diff', js: true, feature: true do
@@ -187,12 +166,14 @@ describe 'Comments', feature: true do
click_diff_line
is_expected.
- to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form",
+ to have_css("form[data-line-code='#{line_code}']",
count: 1)
end
it 'should be removed when canceled' do
- page.within(".diff-file form[id$='#{line_code}']") do
+ is_expected.to have_css('.js-temp-notes-holder')
+
+ page.within("form[data-line-code='#{line_code}']") do
find('.js-close-discussion-note-form').trigger('click')
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 1adab7e9c6c..c7c00a3266a 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -32,7 +32,8 @@ feature 'Member autocomplete', feature: true do
context 'adding a new note on a Issue', js: true do
before do
issue = create(:issue, author: author, project: project)
- create(:note, note: 'Ultralight Beam', noteable: issue, author: participant)
+ create(:note, note: 'Ultralight Beam', noteable: issue,
+ project: project, author: participant)
visit_issue(project, issue)
end
@@ -47,7 +48,8 @@ feature 'Member autocomplete', feature: true do
context 'adding a new note on a Merge Request ', js: true do
before do
merge = create(:merge_request, source_project: project, target_project: project, author: author)
- create(:note, note: 'Ultralight Beam', noteable: merge, author: participant)
+ create(:note, note: 'Ultralight Beam', noteable: merge,
+ project: project, author: participant)
visit_merge_request(project, merge)
end
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
new file mode 100644
index 00000000000..e7ee0aaea3c
--- /dev/null
+++ b/spec/features/pipelines_spec.rb
@@ -0,0 +1,189 @@
+require 'spec_helper'
+
+describe "Pipelines" do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET /:project/pipelines' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') }
+
+ [:all, :running, :branches].each do |scope|
+ context "displaying #{scope}" do
+ let(:project) { create(:project) }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
+
+ it { expect(page).to have_content(pipeline.short_sha) }
+ end
+ end
+
+ context 'anonymous access' do
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_http_status(:success) }
+ end
+
+ context 'cancelable pipeline' do
+ let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-running') }
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it { expect(page).not_to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'retryable pipelines' do
+ let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-failed') }
+
+ context 'when retrying' do
+ before { click_link('Retry') }
+
+ it { expect(page).not_to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-pending') }
+ end
+ end
+
+ context 'for generic statuses' do
+ context 'when running' do
+ let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it 'not be cancelable' do
+ expect(page).not_to have_link('Cancel')
+ end
+
+ it 'pipeline is running' do
+ expect(page).to have_selector('.ci-running')
+ end
+ end
+
+ context 'when failed' do
+ let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it 'not be retryable' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ it 'pipeline is failed' do
+ expect(page).to have_selector('.ci-failed')
+ end
+ end
+ end
+
+ context 'downloadable pipelines' do
+ context 'with artifacts' do
+ let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_selector('.build-artifacts') }
+ it { expect(page).to have_link(with_artifacts.name) }
+ end
+
+ context 'without artifacts' do
+ let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+ end
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it 'showing a list of builds' do
+ expect(page).to have_content('Test')
+ expect(page).to have_content(@success.id)
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content(@failed.id)
+ expect(page).to have_content(@running.id)
+ expect(page).to have_content(@external.id)
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).to have_content('retried') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+ end
+
+ describe 'POST /:project/pipelines' do
+ let(:project) { create(:project) }
+
+ before { visit new_namespace_project_pipeline_path(project.namespace, project) }
+
+ context 'for valid commit' do
+ before { fill_in('Create for', with: 'master') }
+
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
+ end
+
+ context 'without gitlab-ci.yml' do
+ before { click_on 'Create pipeline' }
+
+ it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ end
+ end
+
+ context 'for invalid commit' do
+ before do
+ fill_in('Create for', with: 'invalid reference')
+ click_on 'Create pipeline'
+ end
+
+ it { expect(page).to have_content('Reference not found') }
+ end
+ end
+end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..a85930c7543
--- /dev/null
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'Profile > Personal Access Tokens', feature: true, js: true do
+ let(:user) { create(:user) }
+
+ def active_personal_access_tokens
+ find(".table.active-personal-access-tokens")
+ end
+
+ def inactive_personal_access_tokens
+ find(".table.inactive-personal-access-tokens")
+ end
+
+ def created_personal_access_token
+ find("#created-personal-access-token").value
+ end
+
+ def disallow_personal_access_token_saves!
+ allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
+ errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
+ allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
+ end
+
+ before do
+ login_as(user)
+ end
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text("Never")
+ end
+
+ it "allows creation of a token with an expiry date" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ # Set date to 1st of next month
+ find_field("Expires at").trigger('focus')
+ find("a[title='Next']").click
+ click_on "1"
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ end
+
+ context "when creation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
+ expect(page).to have_content("Name cannot be nil")
+ end
+ end
+ end
+
+ describe "inactive tokens" do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "allows revocation of an active token" do
+ visit profile_personal_access_tokens_path
+ click_on "Revoke"
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ it "moves expired tokens to the 'inactive' section" do
+ personal_access_token.update(expires_at: 5.days.ago)
+ visit profile_personal_access_tokens_path
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ context "when revocation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+
+ expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(page).to have_content("Could not revoke")
+ end
+ end
+ end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 8f645438cff..787bf42d048 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -54,7 +54,7 @@ describe 'Profile > Preferences', feature: true do
end
end
- describe 'User changes their default dashboard' do
+ describe 'User changes their default dashboard', js: true do
it 'creates a flash message' do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
@@ -66,8 +66,10 @@ describe 'Profile > Preferences', feature: true do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
- click_link 'Dashboard'
- expect(page.current_path).to eq starred_dashboard_projects_path
+ allowing_for_delay do
+ find('#logo').click
+ expect(page.current_path).to eq starred_dashboard_projects_path
+ end
click_link 'Your Projects'
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 13c9b95b316..01e90618a98 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -1,19 +1,15 @@
require 'spec_helper'
feature 'list of badges' do
- include Select2Helper
-
background do
user = create(:user)
project = create(:project)
project.team << [user, :master]
login_as(user)
- visit edit_namespace_project_path(project.namespace, project)
+ visit namespace_project_badges_path(project.namespace, project)
end
scenario 'user displays list of badges' do
- click_link 'Badges'
-
expect(page).to have_content 'build status'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
@@ -26,8 +22,11 @@ feature 'list of badges' do
end
scenario 'user changes current ref on badges list page', js: true do
- click_link 'Badges'
- select2('improve/awesome', from: '#ref')
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
expect(page).to have_content 'badges/improve/awesome/build.svg'
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 40ba0bdc115..fcdf7870f34 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -11,16 +11,15 @@ feature 'project commit builds' do
context 'when no builds triggered yet' do
background do
- create(:ci_commit, project: project,
- sha: project.commit.sha,
- ref: 'master')
+ create(:ci_pipeline, project: project,
+ sha: project.commit.sha,
+ ref: 'master')
end
scenario 'user views commit builds page' do
visit builds_namespace_project_commit_path(project.namespace,
project, project.commit.sha)
-
expect(page).to have_content('Builds')
end
end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 0559b02f321..1b4ff6b6f1b 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -5,7 +5,6 @@ describe 'Cherry-pick Commits' do
let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
-
before do
login_as :user
project.team << [@user, :master]
@@ -16,6 +15,7 @@ describe 'Cherry-pick Commits' do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
+ expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
new file mode 100644
index 00000000000..9ebef505b92
--- /dev/null
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+feature 'User wants to add a .gitignore file', feature: true do
+ include WaitForAjax
+
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore')
+ end
+
+ scenario 'user can see .gitignore dropdown' do
+ expect(page).to have_css('.gitignore-selector')
+ end
+
+ scenario 'user can pick a .gitignore file from the dropdown', js: true do
+ find('.js-gitignore-selector').click
+ wait_for_ajax
+ within '.gitignore-selector' do
+ find('.dropdown-input-field').set('rails')
+ find('.dropdown-content li', text: 'Rails').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Rails')
+ expect(page).to have_content('/.bundle')
+ expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
+ end
+end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
new file mode 100644
index 00000000000..fca40f68b01
--- /dev/null
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+feature 'User wants to add a .gitlab-ci.yml file', feature: true do
+ include WaitForAjax
+
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitlab-ci.yml')
+ end
+
+ scenario 'user can see .gitlab-ci.yml dropdown' do
+ expect(page).to have_css('.gitlab-ci-yml-selector')
+ end
+
+ scenario 'user can pick a template from the dropdown', js: true do
+ find('.js-gitlab-ci-yml-selector').click
+ wait_for_ajax
+ within '.gitlab-ci-yml-selector' do
+ find('.dropdown-input-field').set('Jekyll')
+ find('.dropdown-content li', text: 'Jekyll').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Jekyll')
+ expect(page).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(page).to have_content('jekyll build -d test')
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 3d6ffbc4c6b..e1e105e6bbe 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:project) }
@@ -21,11 +21,11 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
click_button 'Commit Changes'
@@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
scenario 'project master creates a license file from the "Add license" link' do
@@ -44,11 +44,11 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
click_button 'Commit Changes'
@@ -56,6 +56,14 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
+ end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
end
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 3268e240200..67aac25e427 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
@@ -20,11 +20,11 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
# Remove pre-receive hook so we can push without auth
@@ -34,6 +34,14 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
+ end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
end
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
new file mode 100644
index 00000000000..bc3bf53fe9d
--- /dev/null
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+feature 'project import', feature: true, js: true do
+ include Select2Helper
+
+ let(:user) { create(:admin) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:project) { Project.last }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ login_as(user)
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ scenario 'user imports an exported project successfully' do
+ expect(Project.all.count).to be_zero
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: 'test-project-path', visible: true
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+
+ attach_file('file', file)
+
+ click_on 'Import project' # import starts
+
+ expect(project).not_to be_nil
+ expect(project.issues).not_to be_empty
+ expect(project.merge_requests).not_to be_empty
+ expect(project.repo_exists?).to be true
+ expect(wiki_exists?).to be true
+ expect(project.import_status).to eq('finished')
+ end
+
+ scenario 'invalid project' do
+ project = create(:project, namespace_id: 2)
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: project.name, visible: true
+ click_link 'GitLab export'
+
+ attach_file('file', file)
+ click_on 'Import project'
+
+ page.within('.flash-container') do
+ expect(page).to have_content('Project could not be imported')
+ end
+ end
+
+ def wiki_exists?
+ wiki = ProjectWiki.new(project)
+ File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
+ end
+end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
new file mode 100644
index 00000000000..7bb0d26b21c
--- /dev/null
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
new file mode 100644
index 00000000000..81b0c991d4f
--- /dev/null
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+feature 'Issue prioritization', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+
+ # Labels
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+ let(:label_4) { create(:label, title: 'label_4', project: project, priority: 4) }
+ let(:label_5) { create(:label, title: 'label_5', project: project) } # no priority
+
+ # According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653
+ context 'when issues have one label' do
+ scenario 'Are sorted properly' do
+ # Issues
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ issue_5 = create(:issue, title: 'issue_5', project: project)
+
+ # Assign labels to issues disorderly
+ issue_4.labels << label_1
+ issue_3.labels << label_2
+ issue_5.labels << label_3
+ issue_2.labels << label_4
+ issue_1.labels << label_5
+
+ login_as user
+ visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+
+ # Ensure we are indicating that issues are sorted by priority
+ expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+
+ page.within('.issues-holder') do
+ issue_titles = all('.issues-list .issue-title-text').map(&:text)
+
+ expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1'])
+ end
+ end
+ end
+
+ context 'when issues have multiple labels' do
+ scenario 'Are sorted properly' do
+ # Issues
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ issue_5 = create(:issue, title: 'issue_5', project: project)
+ issue_6 = create(:issue, title: 'issue_6', project: project)
+ issue_7 = create(:issue, title: 'issue_7', project: project)
+ issue_8 = create(:issue, title: 'issue_8', project: project)
+
+ # Assign labels to issues disorderly
+ issue_5.labels << label_1 # 1
+ issue_5.labels << label_2
+ issue_8.labels << label_1 # 2
+ issue_1.labels << label_2 # 3
+ issue_1.labels << label_3
+ issue_3.labels << label_2 # 4
+ issue_3.labels << label_4
+ issue_7.labels << label_2 # 5
+ issue_2.labels << label_3 # 6
+ issue_4.labels << label_4 # 7
+ issue_6.labels << label_5 # 8 - No priority
+
+ login_as user
+ visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+
+ expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+
+ page.within('.issues-holder') do
+ issue_titles = all('.issues-list .issue-title-text').map(&:text)
+
+ expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8')
+ expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7')
+ expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6'])
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
new file mode 100644
index 00000000000..6a39c302f55
--- /dev/null
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+feature 'Prioritize labels', feature: true do
+ include WaitForAjax
+
+ context 'when project belongs to user' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+
+ scenario 'user can prioritize a label', js: true do
+ bug = create(:label, title: 'bug')
+ wontfix = create(:label, title: 'wontfix')
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('No prioritized labels yet')
+
+ page.within('.other-labels') do
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.prioritized-labels') do
+ expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).to have_content('bug')
+ end
+ end
+
+ scenario 'user can unprioritize a label', js: true do
+ bug = create(:label, title: 'bug', priority: 1)
+ wontfix = create(:label, title: 'wontfix')
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('bug')
+
+ page.within('.prioritized-labels') do
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.other-labels') do
+ expect(page).to have_content('bug')
+ expect(page).to have_content('wontfix')
+ end
+ end
+
+ scenario 'user can sort prioritized labels and persist across reloads', js: true do
+ bug = create(:label, title: 'bug', priority: 1)
+ wontfix = create(:label, title: 'wontfix', priority: 2)
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+
+ # Sort labels
+ find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
+
+ page.within('.prioritized-labels') do
+ expect(first('li')).to have_content('wontfix')
+ expect(page.all('li').last).to have_content('bug')
+ end
+
+ visit current_url
+ wait_for_ajax
+
+ page.within('.prioritized-labels') do
+ expect(first('li')).to have_content('wontfix')
+ expect(page.all('li').last).to have_content('bug')
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'can not prioritize labels' do
+ user = create(:user)
+ guest = create(:user)
+ project = create(:project, name: 'test', namespace: user.namespace)
+
+ create(:label, title: 'bug')
+
+ login_as guest
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).not_to have_css('.prioritized-labels')
+ end
+ end
+
+ context 'as a non signed in user' do
+ it 'can not prioritize labels' do
+ user = create(:user)
+ project = create(:project, name: 'test', namespace: user.namespace)
+
+ create(:label, title: 'bug')
+
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).not_to have_css('.prioritized-labels')
+ end
+ end
+end
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
new file mode 100644
index 00000000000..728c0e16361
--- /dev/null
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group member cannot leave group project', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ background do
+ group.add_developer(user)
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user does not see a "Leave project" link' do
+ expect(page).not_to have_content 'Leave Project'
+ end
+end
diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
new file mode 100644
index 00000000000..ff9b6007806
--- /dev/null
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group member cannot request access to his group project', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ scenario 'owner does not see the request access button' do
+ group.add_owner(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'master does not see the request access button' do
+ group.add_master(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'developer does not see the request access button' do
+ group.add_developer(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'reporter does not see the request access button' do
+ group.add_reporter(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'guest does not see the request access button' do
+ group.add_guest(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ def login_and_visit_project_page(user)
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
new file mode 100644
index 00000000000..c4ed92d2780
--- /dev/null
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ perform_enqueued_jobs { click_link 'Request Access' }
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'group requester does not see the request access / withdraw access request button' do
+ expect(page).not_to have_content 'Request Access'
+ expect(page).not_to have_content 'Withdraw Access Request'
+ end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..f7fcd9b6731
--- /dev/null
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.request_access(user)
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'master can see access requests' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+ end
+
+ scenario 'master can grant access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+ end
+
+ def expect_visible_access_request(project, user)
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{project.name} access requests 1"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
new file mode 100644
index 00000000000..9564347e733
--- /dev/null
+++ b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Member cannot request access to his project', feature: true do
+ let(:member) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [member, :developer]
+ login_as(member)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'member does not see the request access button' do
+ expect(page).not_to have_content 'Request Access'
+ end
+end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
new file mode 100644
index 00000000000..79dec442818
--- /dev/null
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Member leaves project', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user leaves project' do
+ click_link 'Leave Project'
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(project.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
new file mode 100644
index 00000000000..67811b1048e
--- /dev/null
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Owner cannot leave project', feature: true do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [owner, :owner]
+ login_as(owner)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user does not see a "Leave Project" link' do
+ expect(page).not_to have_content 'Leave Project'
+ end
+end
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
new file mode 100644
index 00000000000..0e54c4fdf20
--- /dev/null
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Owner cannot request access to his project', feature: true do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [owner, :owner]
+ login_as(owner)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'owner does not see the request access button' do
+ expect(page).not_to have_content 'Request Access'
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..f2fe3ef364d
--- /dev/null
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Projects > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.team << [master, :master]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user can request access to a project' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Project'
+ end
+
+ scenario 'user is not listed in the project members page' do
+ click_link 'Request Access'
+
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+
+ open_project_settings_menu
+ click_link 'Members'
+
+ visit namespace_project_project_members_path(project.namespace, project)
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(project.requesters.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the project has been withdrawn.'
+ end
+
+ def open_project_settings_menu
+ find('#project-settings-button').click
+ end
+end
diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb
index 2595c4181e5..54aa9c66a08 100644
--- a/spec/features/project/shortcuts_spec.rb
+++ b/spec/features/projects/shortcuts_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Project shortcuts', feature: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, name: 'Victorialand') }
let(:user) { create(:user) }
describe 'On a project', js: true do
@@ -14,7 +14,7 @@ feature 'Project shortcuts', feature: true do
describe 'pressing "i"' do
it 'redirects to new issue page' do
find('body').native.send_key('i')
- expect(page).to have_content('New Issue')
+ expect(page).to have_content('Victorialand')
end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 9dd0378d165..6fa8298d489 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -70,22 +70,6 @@ feature 'Project', feature: true do
end
end
- describe 'leave project link' do
- 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 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
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
new file mode 100644
index 00000000000..d94dee0c797
--- /dev/null
+++ b/spec/features/protected_branches_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+feature 'Projected Branches', feature: true, js: true do
+ let(:user) { create(:user, :admin) }
+ let(:project) { create(:project) }
+
+ before { login_as(user) }
+
+ def set_protected_branch_name(branch_name)
+ find(".js-protected-branch-select").click
+ find(".dropdown-input-field").set(branch_name)
+ click_on "Create Protected Branch: #{branch_name}"
+ end
+
+ describe "explicit protected branches" do
+ it "allows creating explicit protected branches" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('some-branch') }
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.name).to eq('some-branch')
+ end
+
+ it "displays the last commit on the matching branch if it exists" do
+ commit = create(:commit, project: project)
+ project.repository.add_branch(user, 'some-branch', commit.id)
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
+ end
+
+ it "displays an error message if the named branch does not exist" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('some-branch')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('branch was removed') }
+ end
+ end
+
+ describe "wildcard protected branches" do
+ it "allows creating protected branches with a wildcard" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content('*-stable') }
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.name).to eq('*-stable')
+ end
+
+ it "displays the number of matching branches" do
+ project.repository.add_branch(user, 'production-stable', 'master')
+ project.repository.add_branch(user, 'staging-stable', 'master')
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
+
+ within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
+ end
+
+ it "displays all the branches matching the wildcard" do
+ project.repository.add_branch(user, 'production-stable', 'master')
+ project.repository.add_branch(user, 'staging-stable', 'master')
+ project.repository.add_branch(user, 'development', 'master')
+ create(:protected_branch, project: project, name: "*-stable")
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ click_on "2 matching branches"
+
+ within(".protected-branches-list") do
+ expect(page).to have_content("production-stable")
+ expect(page).to have_content("staging-stable")
+ expect(page).not_to have_content("development")
+ end
+ end
+ end
+end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 8edeb8d18af..a5ed3595b0a 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -29,8 +29,8 @@ describe "Runners" do
end
before do
- expect(page).to_not have_content(@specific_runner3.display_name)
- expect(page).to_not have_content(@specific_runner3.display_name)
+ expect(page).not_to have_content(@specific_runner3.display_name)
+ expect(page).not_to have_content(@specific_runner3.display_name)
end
it "places runners in right places" do
@@ -110,4 +110,37 @@ describe "Runners" do
expect(page).to have_content(@specific_runner.platform)
end
end
+
+ feature 'configuring runners ability to picking untagged jobs' do
+ given(:project) { create(:empty_project) }
+ given(:runner) { create(:ci_runner) }
+
+ background do
+ project.team << [user, :master]
+ project.runners << runner
+ end
+
+ scenario 'user checks default configuration' do
+ visit namespace_project_runner_path(project.namespace, project, runner)
+
+ expect(page).to have_content 'Can run untagged jobs Yes'
+ end
+
+ context 'when runner has tags' do
+ before { runner.update_attribute(:tag_list, ['tag']) }
+
+ scenario 'user wants to prevent runner from running untagged job' do
+ visit runners_path(project)
+ page.within('.activated-specific-runners') do
+ first('small > a').click
+ end
+
+ uncheck 'runner_run_untagged'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Can run untagged jobs No'
+ expect(runner.reload.run_untagged?).to eq false
+ end
+ end
+ end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 029a11ea43c..d0a301038c4 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe "Search", feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
+ let!(:issue) { create(:issue, project: project, assignee: user) }
+ let!(:issue2) { create(:issue, project: project, author: user) }
before do
login_with(user)
@@ -47,4 +49,78 @@ describe "Search", feature: true do
expect(page).to have_link(snippet.title)
end
end
+
+ describe 'Right header search field', feature: true do
+ describe 'Search in project page' do
+ before do
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'top right search form is present' do
+ expect(page).to have_selector('#search')
+ end
+
+ it 'top right search form contains location badge' do
+ expect(page).to have_selector('.has-location-badge')
+ end
+
+ context 'clicking the search field', js: true do
+ it 'should show category search dropdown' do
+ page.find('#search').click
+
+ expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+
+ context 'click the links in the category search dropdown', js: true do
+ before do
+ page.find('#search').click
+ end
+
+ it 'should take user to her issues page when issues assigned is clicked' do
+ find('.dropdown-menu').click_link 'Issues assigned to me'
+ sleep 2
+
+ expect(page).to have_selector('.issues-holder')
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her issues page when issues authored is clicked' do
+ find('.dropdown-menu').click_link "Issues I've created"
+ sleep 2
+
+ expect(page).to have_selector('.issues-holder')
+ expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her MR page when MR assigned is clicked' do
+ find('.dropdown-menu').click_link 'Merge requests assigned to me'
+ sleep 2
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her MR page when MR authored is clicked' do
+ find('.dropdown-menu').click_link "Merge requests I've created"
+ sleep 2
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+ end
+
+ context 'entering text into the search field', js: true do
+ before do
+ page.within '.search-input-wrap' do
+ fill_in "search", with: project.name[0..3]
+ end
+ end
+
+ it 'should not display the category search dropdown' do
+ expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index 71b783b7276..35fcef7a712 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -76,7 +76,6 @@ describe 'Internal Group access', feature: true do
it { is_expected.to be_denied_for :visitor }
end
-
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index cc9aee802f9..75a93342628 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -76,7 +76,6 @@ describe 'Private Group access', feature: true do
it { is_expected.to be_denied_for :visitor }
end
-
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index db986683dbe..6c5ee93970b 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -76,7 +76,6 @@ describe 'Public Group access', feature: true do
it { is_expected.to be_allowed_for :visitor }
end
-
describe 'GET /groups/:path/group_members' do
subject { group_group_members_path(group) }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 8625ea6bc10..13d980a326f 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -288,4 +288,142 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/pipelines" do
+ subject { namespace_project_pipelines_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/pipelines/:id" do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/builds" do
+ subject { namespace_project_builds_path(project.namespace, project) }
+
+ context "when allowed for public and internal" do
+ before { project.update(public_builds: true) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ context "when disallowed for public and internal" do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_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(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { namespace_project_build_path(project.namespace, project, build.id) }
+
+ context "when allowed for public and internal" do
+ before { project.update(public_builds: true) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ context "when disallowed for public and internal" do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_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/environments" do
+ subject { namespace_project_environments_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/:id" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/new" do
+ subject { new_namespace_project_environment_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_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/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 544270b4037..ac9690cc127 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -260,4 +260,106 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/pipelines" do
+ subject { namespace_project_pipelines_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/pipelines/:id" do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/builds" do
+ subject { namespace_project_builds_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/builds/:id" do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ subject { namespace_project_build_path(project.namespace, project, build.id) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments" do
+ subject { namespace_project_environments_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/:id" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/new" do
+ subject { new_namespace_project_environment_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_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/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 4def4f99bc0..737897de52b 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -109,6 +109,35 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :external }
end
+ describe "GET /:project_path/pipelines" do
+ subject { namespace_project_pipelines_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ describe "GET /:project_path/pipelines/:id" do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_allowed_for guest }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :external }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
describe "GET /:project_path/builds" do
subject { namespace_project_builds_path(project.namespace, project) }
@@ -142,8 +171,8 @@ describe "Public Project Access", feature: true do
end
describe "GET /:project_path/builds/:id" do
- let(:commit) { create(:ci_commit, project: project) }
- let(:build) { create(:ci_build, commit: commit) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) }
context "when allowed for public" do
@@ -175,6 +204,49 @@ describe "Public Project Access", feature: true do
end
end
+ describe "GET /:project_path/environments" do
+ subject { namespace_project_environments_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/:id" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/new" do
+ subject { new_namespace_project_environment_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_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
let(:commit) { project.repository.commit }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index 58aabd913eb..a752c1d7235 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -2,19 +2,42 @@ require 'spec_helper'
feature 'Signup', feature: true do
describe 'signup with no errors' do
- it 'creates the user account and sends a confirmation email' do
- user = build(:user)
+ context "when sending confirmation email" do
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
- visit root_path
+ it 'creates the user account and sends a confirmation email' do
+ user = build(:user)
- fill_in 'new_user_name', with: user.name
- fill_in 'new_user_username', with: user.username
- fill_in 'new_user_email', with: user.email
- fill_in 'new_user_password', with: user.password
- click_button "Sign up"
+ visit root_path
+
+ fill_in 'new_user_name', with: user.name
+ fill_in 'new_user_username', with: user.username
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_password', with: user.password
+ click_button "Sign up"
+
+ expect(current_path).to eq users_almost_there_path
+ expect(page).to have_content("Please check your email to confirm your account")
+ end
+ end
+
+ context "when not sending confirmation email" do
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
+
+ it 'creates the user account and goes to dashboard' do
+ user = build(:user)
+
+ visit root_path
+
+ fill_in 'new_user_name', with: user.name
+ fill_in 'new_user_username', with: user.username
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_password', with: user.password
+ click_button "Sign up"
- expect(current_path).to eq users_almost_there_path
- expect(page).to have_content("Please check your email to confirm your account")
+ expect(current_path).to eq dashboard_projects_path
+ expect(page).to have_content("Welcome! You have signed up successfully.")
+ end
end
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index f0990118e3c..0f30f562539 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -22,7 +22,6 @@ feature 'Master deletes tag', feature: true do
namespace_project_tags_path(project.namespace, project))
expect(page).not_to have_content 'v1.1.0'
end
-
end
context 'from a specific tag page' do
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index c926e9841f3..6b5b3122f72 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -12,7 +12,7 @@ feature 'Master updates tag', feature: true do
context 'from the tags list page' do
scenario 'updates the release notes' do
- page.within(first('.controls')) do
+ page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index b7368cca29d..6ed279ef9be 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -75,7 +75,10 @@ feature 'Task Lists', feature: true do
describe 'for Notes' do
let!(:issue) { create(:issue, author: user, project: project) }
- let!(:note) { create(:note, note: markdown, noteable: issue, author: user) }
+ let!(:note) do
+ create(:note, note: markdown, noteable: issue,
+ project: project, author: user)
+ end
it 'renders for note body' do
visit_issue(project, issue)
diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb
new file mode 100644
index 00000000000..32fa88a2b21
--- /dev/null
+++ b/spec/features/todos/target_state_spec.rb
@@ -0,0 +1,65 @@
+require 'rails_helper'
+
+feature 'Todo target states', feature: true do
+ let(:user) { create(:user) }
+ let(:author) { create(:user) }
+ let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
+ before do
+ login_as user
+ end
+
+ scenario 'on a closed issue todo has closed label' do
+ issue_closed = create(:issue, state: 'closed')
+ create_todo issue_closed
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Closed')
+ end
+ end
+
+ scenario 'on an open issue todo does not have an open label' do
+ issue_open = create(:issue)
+ create_todo issue_open
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).not_to have_content('Open')
+ end
+ end
+
+ scenario 'on a merged merge request todo has merged label' do
+ mr_merged = create(:merge_request, :simple, author: user, state: 'merged')
+ create_todo mr_merged
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Merged')
+ end
+ end
+
+ scenario 'on a closed merge request todo has closed label' do
+ mr_closed = create(:merge_request, :simple, author: user, state: 'closed')
+ create_todo mr_closed
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Closed')
+ end
+ end
+
+ scenario 'on an open merge request todo does not have an open label' do
+ mr_open = create(:merge_request, :simple, author: user)
+ create_todo mr_open
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).not_to have_content('Open')
+ end
+ end
+
+ def create_todo(target)
+ create(:todo, :mentioned, user: user, project: project, target: target, author: author)
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3354f529295..0bdb1628c74 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) }
let(:author) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:issue) { create(:issue) }
describe 'GET /dashboard/todos' do
@@ -43,6 +43,27 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User has Todos with labels spanning multiple projects' do
+ before do
+ label1 = create(:label, project: project)
+ note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project)
+ create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id)
+
+ project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ label2 = create(:label, project: project2)
+ issue2 = create(:issue, project: project2)
+ note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2)
+ create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows page with two Todos' do
+ expect(page).to have_selector('.todos-list .todo', count: 2)
+ end
+ end
+
context 'User has multiple pages of Todos' do
before do
allow(Todo).to receive(:default_per_page).and_return(1)
@@ -77,5 +98,22 @@ describe 'Dashboard Todos', feature: true do
end
end
end
+
+ context 'User has a Todo in a project pending deletion' do
+ before do
+ deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true)
+ create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author)
+ create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done)
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows "All done" message' do
+ within('.todos-pending-count') { expect(page).to have_content '0' }
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 0'
+ expect(page).to have_content "You're all done!"
+ end
+ end
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
new file mode 100644
index 00000000000..14613754f74
--- /dev/null
+++ b/spec/features/u2f_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+
+feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+ def register_u2f_device(u2f_device = nil)
+ u2f_device ||= FakeU2fDevice.new(page)
+ u2f_device.respond_to_u2f_registration
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+ u2f_device
+ end
+
+ describe "registration" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
+ end
+
+ describe 'when 2FA via OTP is disabled' do
+ before { user.update_attribute(:otp_required_for_login, false) }
+
+ it 'does not allow registering a new device' do
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+
+ expect(page).to have_button('Setup New U2F Device', disabled: true)
+ end
+ end
+
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows registering a new device' do
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+ register_u2f_device
+
+ expect(page.body).to match('Your U2F device was registered')
+ end
+
+ it 'allows registering more than one device' do
+ visit profile_account_path
+
+ # First device
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+
+ # Second device
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+ click_on 'Manage Two-Factor Authentication'
+ expect(page.body).to match('You have 2 U2F devices registered')
+ end
+ end
+
+ it 'allows the same device to be registered for multiple users' do
+ # First user
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ u2f_device = register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+ logout
+
+ # Second user
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device(u2f_device)
+ expect(page.body).to match('Your U2F device was registered')
+
+ expect(U2fRegistration.count).to eq(2)
+ end
+
+ context "when there are form errors" do
+ it "doesn't register the device if there are errors" do
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+
+ # Have the "u2f device" respond with bad data
+ page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+
+ expect(U2fRegistration.count).to eq(0)
+ expect(page.body).to match("The form contains the following error")
+ expect(page.body).to match("did not send a valid JSON response")
+ end
+
+ it "allows retrying registration" do
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+
+ # Failed registration
+ page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+ expect(page.body).to match("The form contains the following error")
+
+ # Successful registration
+ register_u2f_device
+
+ expect(page.body).to match('Your U2F device was registered')
+ expect(U2fRegistration.count).to eq(1)
+ end
+ end
+ end
+
+ describe "authentication" do
+ let(:user) { create(:user) }
+
+ before do
+ # Register and logout
+ login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ @u2f_device = register_u2f_device
+ logout
+ end
+
+ describe "when 2FA via OTP is disabled" do
+ it "allows logging in with the U2F device" do
+ login_with(user)
+
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+
+ describe "when 2FA via OTP is enabled" do
+ it "allows logging in with the U2F device" do
+ user.update_attribute(:otp_required_for_login, true)
+ login_with(user)
+
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+
+ describe "when a given U2F device has already been registered by another user" do
+ describe "but not the current user" do
+ it "does not allow logging in with that particular device" do
+ # Register current user with the different U2F device
+ current_user = login_as(:user)
+ current_user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ logout
+
+ # Try authenticating user with the old U2F device
+ login_as(current_user)
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Authentication via U2F device failed')
+ end
+ end
+
+ describe "and also the current user" do
+ it "allows logging in with that particular device" do
+ # Register current user with the same U2F device
+ current_user = login_as(:user)
+ current_user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device(@u2f_device)
+ logout
+
+ # Try authenticating user with the same U2F device
+ login_as(current_user)
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+ end
+
+ describe "when a given U2F device has not been registered" do
+ it "does not allow logging in with that particular device" do
+ unregistered_device = FakeU2fDevice.new(page)
+ login_as(user)
+ unregistered_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Authentication via U2F device failed')
+ end
+ end
+ end
+
+ describe "when two-factor authentication is disabled" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ end
+
+ it "deletes u2f registrations" do
+ expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index cf116040394..b5a94fe0383 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -47,5 +47,4 @@ feature 'Users', feature: true do
def number_of_errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').count
end
-
end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index afea1840cd7..a2b8f7b6931 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,24 +1,53 @@
require 'spec_helper'
-describe "Variables" do
- let(:user) { create(:user) }
- before { login_as(user) }
-
- describe "specific runners" do
- before do
- @project = FactoryGirl.create :empty_project
- @project.team << [user, :master]
+describe 'Project variables', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:variable) { create(:ci_variable, key: 'test') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ project.variables << variable
+
+ visit namespace_project_variables_path(project.namespace, project)
+ end
+
+ it 'should show list of variables' do
+ page.within('.variables-table') do
+ expect(page).to have_content(variable.key)
+ end
+ end
+
+ it 'should add new variable' do
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'key value')
+ click_button('Add new variable')
+
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ end
+ end
+
+ it 'should delete variable' do
+ page.within('.variables-table') do
+ find('.btn-variable-delete').click
+ end
+
+ expect(page).not_to have_selector('variables-table')
+ end
+
+ it 'should edit variable' do
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
end
- it "creates variable", js: true do
- visit namespace_project_variables_path(@project.namespace, @project)
- click_on "Add a variable"
- fill_in "Key", with: "SECRET_KEY"
- fill_in "Value", with: "SECRET_VALUE"
- click_on "Save changes"
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'key value')
+ click_button('Save variable')
- expect(page).to have_content("Variables were successfully updated.")
- expect(@project.variables.count).to eq(1)
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
end
end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index fdd3849816f..fbe09b28b3c 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -12,14 +12,12 @@ describe GroupProjectsFinder do
let!(:shared_project_2) { create(:project, :private, path: '4') }
let!(:shared_project_3) { create(:project, :internal, path: '5') }
-
before do
shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
shared_project_2.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end
-
describe 'with a group member current user' do
before { group.add_user(current_user, Gitlab::Access::MASTER) }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index bc607a29751..ec8809e6926 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe IssuesFinder do
- let(:user) { create :user }
- let(:user2) { create :user }
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project1) }
let(:label) { create(:label, project: project2) }
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
@@ -16,101 +16,147 @@ describe IssuesFinder do
project1.team << [user, :master]
project2.team << [user, :developer]
project2.team << [user2, :developer]
+
+ issue1
+ issue2
+ issue3
end
- describe :execute do
- before :each do
- issue1
- issue2
- issue3
- end
+ describe '#execute' do
+ let(:search_user) { user }
+ let(:params) { {} }
+ let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute }
context 'scope: all' do
- it 'should filter by all' do
- params = { scope: "all", state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues.size).to eq(3)
+ let(:scope) { 'all' }
+
+ it 'returns all issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
end
- it 'should filter by assignee id' do
- params = { scope: "all", assignee_id: user.id, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues.size).to eq(2)
+ context 'filtering by assignee ID' do
+ let(:params) { { assignee_id: user.id } }
+
+ it 'returns issues assigned to that user' do
+ expect(issues).to contain_exactly(issue1, issue2)
+ end
end
- it 'should filter by author id' do
- params = { scope: "all", author_id: user2.id, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues).to eq([issue3])
+ context 'filtering by author ID' do
+ let(:params) { { author_id: user2.id } }
+
+ it 'returns issues created by that user' do
+ expect(issues).to contain_exactly(issue3)
+ end
end
- it 'should filter by milestone id' do
- params = { scope: "all", milestone_title: milestone.title, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues).to eq([issue1])
+ context 'filtering by milestone' do
+ let(:params) { { milestone_title: milestone.title } }
+
+ it 'returns issues assigned to that milestone' do
+ expect(issues).to contain_exactly(issue1)
+ end
end
- it 'should filter by no milestone id' do
- params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues).to match_array([issue2, issue3])
+ context 'filtering by no milestone' do
+ let(:params) { { milestone_title: Milestone::None.title } }
+
+ it 'returns issues with no milestone' do
+ expect(issues).to contain_exactly(issue2, issue3)
+ end
end
- it 'should filter by label name' do
- params = { scope: "all", label_name: label.title, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues).to eq([issue2])
+ context 'filtering by upcoming milestone' do
+ let(:params) { { milestone_title: Milestone::Upcoming.name } }
+
+ let(:project_no_upcoming_milestones) { create(:empty_project, :public) }
+ let(:project_next_1_1) { create(:empty_project, :public) }
+ let(:project_next_8_8) { create(:empty_project, :public) }
+
+ let(:yesterday) { Date.today - 1.day }
+ let(:tomorrow) { Date.today + 1.day }
+ let(:two_days_from_now) { Date.today + 2.days }
+ let(:ten_days_from_now) { Date.today + 10.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, :closed, project: project_no_upcoming_milestones),
+ create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
+ create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now),
+ create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday),
+ create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow)
+ ]
+ end
+
+ before do
+ milestones.each do |milestone|
+ create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+ end
+ end
+
+ it 'returns issues in the upcoming milestone for each project' do
+ expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8')
+ expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now)
+ end
end
- it 'returns unique issues when filtering by multiple labels' do
- label2 = create(:label, project: project2)
+ context 'filtering by label' do
+ let(:params) { { label_name: label.title } }
- create(:label_link, label: label2, target: issue2)
+ it 'returns issues with that label' do
+ expect(issues).to contain_exactly(issue2)
+ end
+ end
- params = {
- scope: 'all',
- label_name: [label.title, label2.title].join(','),
- state: 'opened'
- }
+ context 'filtering by multiple labels' do
+ let(:params) { { label_name: [label.title, label2.title].join(',') } }
+ let(:label2) { create(:label, project: project2) }
- issues = IssuesFinder.new(user, params).execute
+ before { create(:label_link, label: label2, target: issue2) }
- expect(issues).to eq([issue2])
+ it 'returns the unique issues with any of those labels' do
+ expect(issues).to contain_exactly(issue2)
+ end
end
- it 'should filter by no label name' do
- params = { scope: "all", label_name: Label::None.title, state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues).to match_array([issue1, issue3])
+ context 'filtering by no label' do
+ let(:params) { { label_name: Label::None.title } }
+
+ it 'returns issues with no labels' do
+ expect(issues).to contain_exactly(issue1, issue3)
+ end
end
- it 'should be empty for unauthorized user' do
- params = { scope: "all", state: 'opened' }
- issues = IssuesFinder.new(nil, params).execute
- expect(issues.size).to be_zero
+ context 'when the user is unauthorized' do
+ let(:search_user) { nil }
+
+ it 'returns no results' do
+ expect(issues).to be_empty
+ end
end
- it 'should not include unauthorized issues' do
- params = { scope: "all", state: 'opened' }
- issues = IssuesFinder.new(user2, params).execute
- expect(issues.size).to eq(2)
- expect(issues).not_to include(issue1)
- expect(issues).to include(issue2)
- expect(issues).to include(issue3)
+ context 'when the user can see some, but not all, issues' do
+ let(:search_user) { user2 }
+
+ it 'returns only issues they can see' do
+ expect(issues).to contain_exactly(issue2, issue3)
+ end
end
end
context 'personal scope' do
- it 'should filter by assignee' do
- params = { scope: "assigned-to-me", state: 'opened' }
- issues = IssuesFinder.new(user, params).execute
- expect(issues.size).to eq(2)
+ let(:scope) { 'assigned-to-me' }
+
+ it 'returns issue assigned to the user' do
+ expect(issues).to contain_exactly(issue1, issue2)
end
- it 'should filter by project' do
- params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id }
- issues = IssuesFinder.new(user, params).execute
- expect(issues.size).to eq(1)
+ context 'filtering by project' do
+ let(:params) { { project_id: project1.id } }
+
+ it 'returns issues assigned to the user in that project' do
+ expect(issues).to contain_exactly(issue1)
+ end
end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index c83824b900d..1bd354815e4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -34,5 +34,28 @@ describe NotesFinder do
notes = NotesFinder.new.execute(project, user, params)
expect(notes).to eq([note1])
end
+
+ context 'confidential issue notes' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+
+ let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+
+ it 'returns notes if user can see the issue' do
+ expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+ end
+
+ it 'raises an error if user can not see the issue' do
+ user = create(:user)
+ expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises an error for project members with guest role' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 810016c9658..28bdc18e840 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -69,7 +69,6 @@ describe SnippetsFinder do
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet2, @snippet1)
end
-
end
context 'by_project filter' do
diff --git a/spec/fixtures/container_registry/config_blob.json b/spec/fixtures/container_registry/config_blob.json
new file mode 100644
index 00000000000..1028c994a24
--- /dev/null
+++ b/spec/fixtures/container_registry/config_blob.json
@@ -0,0 +1 @@
+{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}}
diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json
new file mode 100644
index 00000000000..8d1b874c29b
--- /dev/null
+++ b/spec/fixtures/container_registry/tag_manifest.json
@@ -0,0 +1,16 @@
+{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "config": {
+ "mediaType": "application/octet-stream",
+ "size": 1145,
+ "digest": "sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 2319870,
+ "digest": "sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"
+ }
+ ]
+}
diff --git a/spec/fixtures/container_registry/tag_manifest_1.json b/spec/fixtures/container_registry/tag_manifest_1.json
new file mode 100644
index 00000000000..d09ede5bea7
--- /dev/null
+++ b/spec/fixtures/container_registry/tag_manifest_1.json
@@ -0,0 +1,32 @@
+{
+ "schemaVersion": 1,
+ "name": "library/alpine",
+ "tag": "2.6",
+ "architecture": "amd64",
+ "fsLayers": [
+ {
+ "blobSum": "sha256:2a3ebcb7fbcc29bf40c4f62863008bb573acdea963454834d9483b3e5300c45d"
+ }
+ ],
+ "history": [
+ {
+ "v1Compatibility": "{\"id\":\"dd807873c9a21bcc82e30317c283e6601d7e19f5cf7867eec34cdd1aeb3f099e\",\"created\":\"2016-01-18T18:32:39.162138276Z\",\"container\":\"556a728876db7b0e621adc029c87c649d32520804f8f15defd67bb070dc1a88d\",\"container_config\":{\"Hostname\":\"556a728876db\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:7dee8a455bcc39013aa168d27ece9227aad155adbaacbd153d94ca60113f59fc in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.3\",\"config\":{\"Hostname\":\"556a728876db\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":4501436}"
+ }
+ ],
+ "signatures": [
+ {
+ "header": {
+ "jwk": {
+ "crv": "P-256",
+ "kid": "4MZL:Z5ZP:2RPA:Q3TD:QOHA:743L:EM2G:QY6Q:ZJCX:BSD7:CRYC:LQ6T",
+ "kty": "EC",
+ "x": "qmWOaxPUk7QsE5iTPdeG1e9yNE-wranvQEnWzz9FhWM",
+ "y": "WeeBpjTOYnTNrfCIxtFY5qMrJNNk9C1vc5ryxbbMD_M"
+ },
+ "alg": "ES256"
+ },
+ "signature": "0zmjTJ4m21yVwAeteLc3SsQ0miScViCDktFPR67W-ozGjjI3iBjlDjwOl6o2sds5ZI9U6bSIKOeLDinGOhHoOQ",
+ "protected": "eyJmb3JtYXRMZW5ndGgiOjEzNzIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNi0xNVQxMDo0NDoxNFoifQ"
+ }
+ ]
+}
diff --git a/spec/fixtures/dk.png b/spec/fixtures/dk.png
index 87ce25e877a..1247f2fecd7 100644
--- a/spec/fixtures/dk.png
+++ b/spec/fixtures/dk.png
Binary files differ
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 1772cc3f6a4..c75d28d9801 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags:
### ExternalLinkFilter
-External links get a `rel="nofollow"` attribute:
+External links get a `rel="nofollow noreferrer"` and `target="_blank"` attributes:
- [Google](https://google.com/)
- [GitLab Root](<%= Gitlab.config.gitlab.url %>)
@@ -216,10 +216,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
#### MilestoneReferenceFilter
-- Milestone: <%= milestone.to_reference %>
+- Milestone by ID: <%= simple_milestone.to_reference %>
+- Milestone by name: <%= Milestone.reference_prefix %><%= simple_milestone.name %>
+- Milestone by name in quotes: <%= milestone.to_reference(format: :name) %>
- 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) %>)
+- Ignored in code: `<%= simple_milestone.to_reference %>`
+- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
+- Milestone by URL: <%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>
+- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
### Task Lists
@@ -239,3 +243,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [[link-text|http://example.com/pdfs/gollum.pdf]]
- [[images/example.jpg]]
- [[http://example.com/images/example.jpg]]
+
+### Inline Diffs
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags can not be mixed as such -
+
+- {+ additions +]
+- [+ additions +}
+- {- delletions -]
+- [- delletions -}
diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml
index a8b7907d4ba..7d01183e3ef 100644
--- a/spec/fixtures/parallel_diff_result.yml
+++ b/spec/fixtures/parallel_diff_result.yml
@@ -3,322 +3,818 @@
:type: match
:number: 6
:text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :line_code:
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: match
:number: 6
:text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :line_code:
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number: 6
:text: |2
<span id="LC6" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 6
+ :new_line: 6
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type:
:number: 6
:text: |2
<span id="LC6" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 6
+ :new_line: 6
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 7
+ :new_line: 7
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 7
+ :new_line: 7
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 8
+ :new_line: 8
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 8
+ :new_line: 8
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 9
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 9
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number: 10
:text: |2
<span id="LC10" class="line"> <span class="k">end</span></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 10
+ :new_line: 10
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type:
:number: 10
:text: |2
<span id="LC10" class="line"> <span class="k">end</span></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 10
+ :new_line: 10
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number: 11
:text: |2
<span id="LC11" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 11
+ :new_line: 11
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type:
:number: 11
:text: |2
<span id="LC11" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 11
+ :new_line: 11
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 12
+ :new_line: 12
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 12
+ :new_line: 12
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 13
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: old
:number:
:text: ''
:line_code:
+ :position:
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 14
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: new
:number: 13
:text: |
+<span id="LC13" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 13
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 14
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 14
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 15
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 15
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 16
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: new
:number: 16
:text: |
+<span id="LC16" class="line"> <span class="p">}</span></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 16
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 17
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: new
:number: 17
:text: |
+<span id="LC17" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 17
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 18
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 18
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 19
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 19
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 20
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: new
:number: 20
:text: |
+<span id="LC20" class="line"> <span class="p">}</span></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 20
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number: 15
:text: |2
<span id="LC21" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 15
+ :new_line: 21
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type:
:number: 21
:text: |2
<span id="LC21" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 15
+ :new_line: 21
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 16
+ :new_line: 22
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 16
+ :new_line: 22
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 17
+ :new_line: 23
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 17
+ :new_line: 23
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type: match
:number: 19
:text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :line_code:
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: match
:number: 25
:text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :line_code:
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line:
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number: 19
:text: |2
<span id="LC25" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 19
+ :new_line: 25
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type:
:number: 25
:text: |2
<span id="LC25" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 19
+ :new_line: 25
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 20
+ :new_line: 26
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 20
+ :new_line: 26
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 21
+ :new_line: 27
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 21
+ :new_line: 27
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :left:
:type:
:number:
:text: ''
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 28
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
:right:
:type: new
:number: 28
:text: |
+<span id="LC28" class="line"></span>
:line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line:
+ :new_line: 28
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 22
+ :new_line: 29
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 22
+ :new_line: 29
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 23
+ :new_line: 30
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 23
+ :new_line: 30
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 24
+ :new_line: 31
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
: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
+ :position: !ruby/object:Gitlab::Diff::Position
+ attributes:
+ :old_path: files/ruby/popen.rb
+ :new_path: files/ruby/popen.rb
+ :old_line: 24
+ :new_line: 31
+ :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f6c1005d265..bb28866f010 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -174,51 +174,6 @@ describe ApplicationHelper do
end
end
- describe 'grouped_options_refs' do
- let(:options) { helper.grouped_options_refs }
- let(:project) { create(:project) }
-
- before do
- assign(:project, project)
-
- # Override Rails' grouped_options_for_select helper to just return the
- # first argument (`options`), since it's easier to work with than the
- # generated HTML.
- allow(helper).to receive(:grouped_options_for_select).
- and_wrap_original { |_, *args| args.first }
- end
-
- it 'includes a list of branch names' do
- expect(options[0][0]).to eq('Branches')
- expect(options[0][1]).to include('master', 'feature')
- end
-
- it 'includes a list of tag names' do
- expect(options[1][0]).to eq('Tags')
- expect(options[1][1]).to include('v1.0.0', 'v1.1.0')
- end
-
- it 'includes a specific commit ref if defined' do
- # Must be an instance variable
- ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8'
- assign(:ref, ref)
-
- expect(options[2][0]).to eq('Commit')
- expect(options[2][1]).to eq([ref])
- end
-
- it 'sorts tags in a natural order' do
- # Stub repository.tag_names to make sure we get some valid testing data
- expect(project.repository).to receive(:tag_names).
- and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿',
- 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2'])
-
- expect(options[1][1]).
- to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10',
- 'v1.0.9', 'v1.0.9a'])
- end
- end
-
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index e47a54fdac5..49ea4fa6d3e 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
describe AuthHelper do
describe "button_based_providers" do
- it 'returns all enabled providers' do
+ it 'returns all enabled providers from devise' do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
expect(helper.button_based_providers).to include(*[:twitter, :github])
end
@@ -17,4 +17,49 @@ describe AuthHelper do
expect(helper.button_based_providers).to eq([])
end
end
+
+ describe 'enabled_button_based_providers' do
+ before do
+ allow(helper).to receive(:auth_providers) { [:twitter, :github] }
+ end
+
+ context 'all providers are enabled to sign in' do
+ it 'returns all the enabled providers from settings' do
+ expect(helper.enabled_button_based_providers).to include('twitter', 'github')
+ end
+ end
+
+ context 'GitHub OAuth sign in is disabled from application setting' do
+ it "doesn't return github as provider" do
+ stub_application_setting(
+ disabled_oauth_sign_in_sources: ['github']
+ )
+
+ expect(helper.enabled_button_based_providers).to include('twitter')
+ expect(helper.enabled_button_based_providers).not_to include('github')
+ end
+ end
+ end
+
+ describe 'button_based_providers_enabled?' do
+ before do
+ allow(helper).to receive(:auth_providers) { [:twitter, :github] }
+ end
+
+ context 'button based providers enabled' do
+ it 'returns true' do
+ expect(helper.button_based_providers_enabled?).to be true
+ end
+ end
+
+ context 'all the button based providers are disabled via application_setting' do
+ it 'returns false' do
+ stub_application_setting(
+ disabled_oauth_sign_in_sources: ['github', 'twitter']
+ )
+
+ expect(helper.button_based_providers_enabled?).to be false
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index f942695b6f0..45199d0f09d 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe CiStatusHelper do
include IconsHelper
- let(:success_commit) { double("Ci::Commit", status: 'success') }
- let(:failed_commit) { double("Ci::Commit", status: 'failed') }
+ let(:success_commit) { double("Ci::Pipeline", status: 'success') }
+ let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
describe 'ci_icon_for_status' do
it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') }
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index b7810185d16..e2db33d8345 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -9,7 +9,7 @@ describe DiffHelper do
let(:diffs) { commit.diffs }
let(:diff) { diffs.first }
let(:diff_refs) { [commit.parent, commit] }
- let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
describe 'diff_view' do
it 'returns a valid value when cookie is set' do
@@ -93,9 +93,9 @@ describe DiffHelper do
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 eq("abc <span class='idiff left right deletion'>&#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 eq("abc <span class='idiff left right addition'>&quot;def&quot;</span>")
expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index e68a5ec29ab..c0d2be98e85 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -1,64 +1,65 @@
require 'spec_helper'
describe EventsHelper do
- include ApplicationHelper
- include GitlabMarkdownHelper
+ describe '#event_note' do
+ before do
+ allow(helper).to receive(:current_user).and_return(double)
+ end
- let(:current_user) { create(:user, email: "current@email.com") }
+ it 'should display one line of plain text without alteration' do
+ input = 'A short, plain note'
+ expect(helper.event_note(input)).to match(input)
+ expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
+ end
- it 'should display one line of plain text without alteration' do
- input = 'A short, plain note'
- expect(event_note(input)).to match(input)
- expect(event_note(input)).not_to match(/\.\.\.\z/)
- end
+ it 'should display inline code' do
+ input = 'A note with `inline code`'
+ expected = 'A note with <code>inline code</code>'
- it 'should display inline code' do
- input = 'A note with `inline code`'
- expected = 'A note with <code>inline code</code>'
+ expect(helper.event_note(input)).to match(expected)
+ end
- expect(event_note(input)).to match(expected)
- end
+ it 'should truncate a note with multiple paragraphs' do
+ input = "Paragraph 1\n\nParagraph 2"
+ expected = 'Paragraph 1...'
- it 'should truncate a note with multiple paragraphs' do
- input = "Paragraph 1\n\nParagraph 2"
- expected = 'Paragraph 1...'
+ expect(helper.event_note(input)).to match(expected)
+ end
- expect(event_note(input)).to match(expected)
- end
+ it 'should display the first line of a code block' do
+ input = "```\nCode block\nwith two lines\n```"
+ expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
- it 'should display the first line of a code block' do
- input = "```\nCode block\nwith two lines\n```"
- expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
+ expect(helper.event_note(input)).to match(expected)
+ end
- expect(event_note(input)).to match(expected)
- end
+ it 'should truncate a single long line of text' do
+ text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
+ input = text * 4
+ expected = (text * 2).sub(/.{3}/, '...')
- it 'should truncate a single long line of text' do
- text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
- input = "#{text}#{text}#{text}#{text}" # 200 chars
- expected = "#{text}#{text}".sub(/.{3}/, '...')
+ expect(helper.event_note(input)).to match(expected)
+ end
- expect(event_note(input)).to match(expected)
- end
-
- it 'should preserve a link href when link text is truncated' do
- text = 'The quick brown fox jumped over the lazy dog' # 44 chars
- input = "#{text}#{text}#{text} " # 133 chars
- link_url = 'http://example.com/foo/bar/baz' # 30 chars
- input << link_url
- expected_link_text = 'http://example...</a>'
+ it 'should preserve a link href when link text is truncated' do
+ text = 'The quick brown fox jumped over the lazy dog' # 44 chars
+ input = "#{text}#{text}#{text} " # 133 chars
+ link_url = 'http://example.com/foo/bar/baz' # 30 chars
+ input << link_url
+ expected_link_text = 'http://example...</a>'
- expect(event_note(input)).to match(link_url)
- expect(event_note(input)).to match(expected_link_text)
- end
+ expect(helper.event_note(input)).to match(link_url)
+ expect(helper.event_note(input)).to match(expected_link_text)
+ end
- it 'should preserve code color scheme' do
- input = "```ruby\ndef test\n 'hello world'\nend\n```"
- expected = '<pre class="code highlight js-syntax-highlight ruby">' \
- "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
- " <span class=\"s1\">\'hello world\'</span>\n" \
- "<span class=\"k\">end</span>" \
- '</code></pre>'
- expect(event_note(input)).to eq(expected)
+ it 'should preserve code color scheme' do
+ input = "```ruby\ndef test\n 'hello world'\nend\n```"
+ expected = '<pre class="code highlight js-syntax-highlight ruby">' \
+ "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
+ " <span class=\"s1\">\'hello world\'</span>\n" \
+ "<span class=\"k\">end</span>" \
+ '</code></pre>'
+ expect(helper.event_note(input)).to eq(expected)
+ end
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 13de88e2f21..ade5c3b02d9 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
+ allow(@wiki).to receive(:slug).and_return('nested/page')
helper.instance_variable_set(:@project_wiki, @wiki)
end
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', pipeline: :wiki, project_wiki: @wiki)
+ expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki)
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
new file mode 100644
index 00000000000..14847d0a49e
--- /dev/null
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe GitlabRoutingHelper do
+ describe 'Project URL helpers' do
+ describe '#project_members_url' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project_members_url(project)).to eq namespace_project_project_members_url(project.namespace, project) }
+ end
+
+ describe '#project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(project_member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#request_access_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(request_access_project_members_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#leave_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(leave_project_members_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#approve_access_request_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(approve_access_request_project_member_path(project_member)).to eq approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#resend_invite_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(resend_invite_project_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+ end
+
+ describe 'Group URL helpers' do
+ describe '#group_members_url' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(group_members_url(group)).to eq group_group_members_url(group) }
+ end
+
+ describe '#group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(group_member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#request_access_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(request_access_group_members_path(group)).to eq request_access_group_group_members_path(group) }
+ end
+
+ describe '#leave_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(leave_group_members_path(group)).to eq leave_group_group_members_path(group) }
+ end
+
+ describe '#approve_access_request_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(approve_access_request_group_member_path(group_member)).to eq approve_access_request_group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#resend_invite_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
+ end
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index bffe2c18b6f..831ae7fb69c 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -7,10 +7,7 @@ describe IssuesHelper do
describe "url_for_project_issues" do
let(:project_url) { ext_project.external_issue_tracker.project_url }
- let(:ext_expected) do
- project_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project]) }
it "should return internal path if used internal tracker" do
@@ -56,11 +53,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
- let(:ext_expected) do
- issues_url.gsub(':id', issue.iid.to_s)
- .gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
it "should return internal path if used internal tracker" do
@@ -106,10 +99,7 @@ describe IssuesHelper do
describe 'url_for_new_issue' do
let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- let(:ext_expected) do
- issues_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
it "should return internal path if used internal tracker" do
@@ -163,18 +153,15 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
- describe "note_active_class" do
- before do
- @note = create :note
- @note1 = create :note
- end
+ describe '#award_active_class' do
+ let!(:upvote) { create(:award_emoji) }
it "returns empty string for unauthenticated user" do
- expect(note_active_class(Note.all, nil)).to eq("")
+ expect(award_active_class(AwardEmoji.all, nil)).to eq("")
end
it "returns active string for author" do
- expect(note_active_class(Note.all, @note.author)).to eq("active")
+ expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
new file mode 100644
index 00000000000..f75fdb739f6
--- /dev/null
+++ b/spec/helpers/members_helper_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe MembersHelper do
+ describe '#action_member_permission' do
+ let(:project_member) { build(:project_member) }
+ let(:group_member) { build(:group_member) }
+
+ it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
+ it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
+ end
+
+ describe '#default_show_roles' do
+ let(:user) { double }
+ let(:member) { build(:project_member) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
+ end
+
+ context 'when the current cannot update, destroy or admin the passed member' do
+ it 'returns false' do
+ expect(helper.default_show_roles(member)).to be_falsy
+ end
+ end
+
+ context 'when the current can update the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can destroy the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can admin the passed member source' do
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+ end
+
+ describe '#remove_member_message' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
+ end
+
+ describe '#remove_member_title' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
+ it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
+ it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
+ it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
+ end
+
+ describe '#leave_confirmation_message' do
+ let(:project) { build_stubbed(:project) }
+ let(:group) { build_stubbed(:group) }
+ let(:user) { build_stubbed(:user) }
+
+ it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+ it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
+ end
+end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 600e1c4e9ec..903224589dd 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequestsHelper do
let(:project) { create :project }
let(:merge_request) { MergeRequest.new }
let(:ci_service) { CiService.new }
- let(:last_commit) { Ci::Commit.new({}) }
+ let(:last_commit) { Ci::Pipeline.new({}) }
before do
allow(merge_request).to receive(:source_project).and_return(project)
@@ -17,7 +17,7 @@ describe MergeRequestsHelper do
it 'does not include api credentials in a link' do
allow(ci_service).
to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c")
- expect(helper.ci_build_details_path(merge_request)).to_not match("secret")
+ expect(helper.ci_build_details_path(merge_request)).not_to match("secret")
end
end
@@ -33,9 +33,9 @@ describe MergeRequestsHelper do
let(:project) { create(:project) }
let(:issues) do
[
- JiraIssue.new('JIRA-123', project),
- JiraIssue.new('JIRA-456', project),
- JiraIssue.new('FOOBAR-7890', project)
+ ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('JIRA-456', project),
+ ExternalIssue.new('FOOBAR-7890', project)
]
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
new file mode 100644
index 00000000000..08a93503258
--- /dev/null
+++ b/spec/helpers/notes_helper_spec.rb
@@ -0,0 +1,46 @@
+require "spec_helper"
+
+describe NotesHelper do
+ describe "#notes_max_access_for_users" do
+ let(:owner) { create(:owner) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:owner_note) { create(:note, author: owner, project: project) }
+ let(:master_note) { create(:note, author: master, project: project) }
+ let(:reporter_note) { create(:note, author: reporter, project: project) }
+ let!(:notes) { [owner_note, master_note, reporter_note] }
+
+ before do
+ group.add_owner(owner)
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ it 'return human access levels' do
+ original_method = project.team.method(:human_max_access)
+ expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args|
+ original_method.call(args[1])
+ end
+
+ expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
+ expect(helper.note_max_access_for_user(master_note)).to eq('Master')
+ expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
+ # Call it again to ensure value is cached
+ expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
+ end
+
+ it 'handles access in different projects' do
+ second_project = create(:empty_project)
+ second_project.team << [master, :reporter]
+ other_note = create(:note, author: master, project: second_project)
+
+ expect(helper.note_max_access_for_user(master_note)).to eq('Master')
+ expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ac5af8740dc..604204cca0a 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -45,16 +45,6 @@ describe ProjectsHelper do
end
end
- describe 'user_max_access_in_project' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- before do
- project.team.add_user(user, Gitlab::Access::MASTER)
- end
-
- it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
- end
-
describe "readme_cache_key" do
let(:project) { create(:project) }
@@ -133,11 +123,17 @@ describe ProjectsHelper do
end
describe '#sanitized_import_error' do
+ let(:project) { create(:project) }
+
+ before do
+ allow(project).to receive(:repository_storage_path).and_return('/base/repo/path')
+ end
+
it 'removes the repo path' do
- repo = File.join(Gitlab.config.gitlab_shell.repos_path, '/namespace/test.git')
+ repo = '/base/repo/path/namespace/test.git'
import_error = "Could not clone #{repo}\n"
- expect(sanitize_repo_path(import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
+ expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
end
end
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index ff98249570d..db3ad1b99e9 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -1,12 +1,6 @@
require 'spec_helper'
describe VisibilityLevelHelper do
- include Haml::Helpers
-
- before :all do
- init_haml_helpers
- end
-
let(:project) { build(:project) }
let(:group) { build(:group) }
let(:personal_snippet) { build(:personal_snippet) }
@@ -95,6 +89,5 @@ describe VisibilityLevelHelper do
expect(skip_level?(snippet, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
end
end
-
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
new file mode 100644
index 00000000000..5178bd130f4
--- /dev/null
+++ b/spec/initializers/6_validations_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe '6_validations', lib: true do
+ context 'with correct settings' do
+ before do
+ mock_storages('foo' => '/a/b/c', 'bar' => 'a/b/d')
+ end
+
+ it 'passes through' do
+ expect { load_validations }.not_to raise_error
+ end
+ end
+
+ context 'with invalid storage names' do
+ before do
+ mock_storages('name with spaces' => '/a/b/c')
+ end
+
+ it 'throws an error' do
+ expect { load_validations }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+ end
+ end
+
+ context 'with nested storage paths' do
+ before do
+ mock_storages('foo' => '/a/b/c', 'bar' => '/a/b/c/d')
+ end
+
+ it 'throws an error' do
+ expect { load_validations }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+ end
+ end
+
+ def mock_storages(storages)
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ def load_validations
+ load File.join(__dir__, '../../config/initializers/6_validations.rb')
+ end
+end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index e58f2c80e95..47b4e431823 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -1,7 +1,7 @@
+require 'spec_helper'
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
@@ -40,5 +40,4 @@ describe Settings, lib: true do
end
end
end
-
end
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 4bb149f25ff..14c8df954a6 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -6,14 +6,16 @@ describe 'trusted_proxies', lib: true do
set_trusted_proxies([])
end
- it 'preserves private IPs as remote_ip' do
+ it 'preserves private IPs' do
request = stub_request('HTTP_X_FORWARDED_FOR' => '10.1.5.89')
expect(request.remote_ip).to eq('10.1.5.89')
+ expect(request.ip).to eq('10.1.5.89')
end
- it 'filters out localhost from remote_ip' do
+ it 'filters out localhost' do
request = stub_request('HTTP_X_FORWARDED_FOR' => '1.1.1.1, 10.1.5.89, 127.0.0.1')
expect(request.remote_ip).to eq('10.1.5.89')
+ expect(request.ip).to eq('10.1.5.89')
end
end
@@ -22,9 +24,10 @@ describe 'trusted_proxies', lib: true do
set_trusted_proxies([ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ])
end
- it 'filters out private and local IPs from remote_ip' do
+ it 'filters out private and local IPs' do
request = stub_request('HTTP_X_FORWARDED_FOR' => '1.2.3.6, 1.1.1.1, 10.1.5.89, 127.0.0.1')
expect(request.remote_ip).to eq('1.1.1.1')
+ expect(request.ip).to eq('1.1.1.1')
end
end
@@ -33,9 +36,10 @@ describe 'trusted_proxies', lib: true do
set_trusted_proxies([ "60.98.25.47" ])
end
- it 'filters out proxy IP from remote_ip' do
+ it 'filters out proxy IP' do
request = stub_request('HTTP_X_FORWARDED_FOR' => '1.2.3.6, 1.1.1.1, 60.98.25.47, 127.0.0.1')
expect(request.remote_ip).to eq('1.1.1.1')
+ expect(request.ip).to eq('1.1.1.1')
end
end
diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee
new file mode 100644
index 00000000000..4b6a2bb5440
--- /dev/null
+++ b/spec/javascripts/application_spec.js.coffee
@@ -0,0 +1,30 @@
+#= require lib/utils/common_utils
+
+describe 'Application', ->
+ describe 'disable buttons', ->
+ fixture.preload('application.html')
+
+ beforeEach ->
+ fixture.load('application.html')
+
+ it 'should prevent default action for disabled buttons', ->
+
+ gl.utils.preventDisabledButtons()
+
+ isClicked = false
+ $button = $ '#test-button'
+
+ $button.click -> isClicked = true
+ $button.trigger 'click'
+
+ expect(isClicked).toBe false
+
+
+ it 'should be on the same page if a disabled link clicked', ->
+
+ locationBeforeLinkClick = window.location.href
+ gl.utils.preventDisabledButtons()
+
+ $('#test-link').click()
+
+ expect(window.location.href).toBe locationBeforeLinkClick
diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee
new file mode 100644
index 00000000000..d7f9c6fc076
--- /dev/null
+++ b/spec/javascripts/awards_handler_spec.js.coffee
@@ -0,0 +1,200 @@
+#= require awards_handler
+#= require jquery
+#= require jquery.cookie
+#= require ./fixtures/emoji_menu
+
+awardsHandler = null
+window.gl or= {}
+window.gon or= {}
+gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
+gon.award_menu_url = '/emojis'
+
+
+lazyAssert = (done, assertFn) ->
+
+ setTimeout -> # Maybe jasmine.clock here?
+ assertFn()
+ done()
+ , 333
+
+
+describe 'AwardsHandler', ->
+
+ fixture.preload 'awards_handler.html'
+
+ beforeEach ->
+ fixture.load 'awards_handler.html'
+ awardsHandler = new AwardsHandler
+ spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
+ spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu
+
+
+ describe '::showEmojiMenu', ->
+
+ it 'should show emoji menu when Add emoji button clicked', (done) ->
+
+ $('.js-add-award').eq(0).click()
+
+ lazyAssert done, ->
+ $emojiMenu = $ '.emoji-menu'
+ expect($emojiMenu.length).toBe 1
+ expect($emojiMenu.hasClass('is-visible')).toBe yes
+ expect($emojiMenu.find('#emoji_search').length).toBe 1
+ expect($('.js-awards-block.current').length).toBe 1
+
+
+ it 'should also show emoji menu for the smiley icon in notes', (done) ->
+
+ $('.note-action-button').click()
+
+ lazyAssert done, ->
+ $emojiMenu = $ '.emoji-menu'
+ expect($emojiMenu.length).toBe 1
+
+
+ it 'should remove emoji menu when body is clicked', (done) ->
+
+ $('.js-add-award').eq(0).click()
+
+ lazyAssert done, ->
+ $emojiMenu = $('.emoji-menu')
+ $('body').click()
+ expect($emojiMenu.length).toBe 1
+ expect($emojiMenu.hasClass('is-visible')).toBe no
+ expect($('.js-awards-block.current').length).toBe 0
+
+
+ describe '::addAwardToEmojiBar', ->
+
+ it 'should add emoji to votes block', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+
+ expect($emojiButton.length).toBe 1
+ expect($emojiButton.next('.js-counter').text()).toBe '1'
+ expect($votesBlock.hasClass('hidden')).toBe no
+
+
+ it 'should remove the emoji when we click again', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+
+ expect($emojiButton.length).toBe 0
+
+
+ it 'should decrement the emoji counter', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+ $emojiButton.next('.js-counter').text 5
+
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ expect($emojiButton.length).toBe 1
+ expect($emojiButton.next('.js-counter').text()).toBe '4'
+
+
+ describe '::getAwardUrl', ->
+
+ it 'should return the url for request', ->
+
+ expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'
+
+
+ describe '::addAward and ::checkMutuality', ->
+
+ it 'should handle :+1: and :-1: mutuality', ->
+
+ awardUrl = awardsHandler.getAwardUrl()
+ $votesBlock = $('.js-awards-block').eq 0
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent()
+ $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent()
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no
+
+ expect($thumbsUpEmoji.hasClass('active')).toBe yes
+ expect($thumbsDownEmoji.hasClass('active')).toBe no
+
+ $thumbsUpEmoji.tooltip()
+ $thumbsDownEmoji.tooltip()
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes
+
+ expect($thumbsUpEmoji.hasClass('active')).toBe no
+ expect($thumbsDownEmoji.hasClass('active')).toBe yes
+
+
+ describe '::removeEmoji', ->
+
+ it 'should remove emoji', ->
+
+ awardUrl = awardsHandler.getAwardUrl()
+ $votesBlock = $('.js-awards-block').eq 0
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'fire', no
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe 1
+
+ awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button')
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe 0
+
+
+ describe 'search', ->
+
+ it 'should filter the emoji', ->
+
+ $('.js-add-award').eq(0).click()
+
+ expect($('[data-emoji=angel]').is(':visible')).toBe yes
+ expect($('[data-emoji=anger]').is(':visible')).toBe yes
+
+ $('#emoji_search').val('ali').trigger 'keyup'
+
+ expect($('[data-emoji=angel]').is(':visible')).toBe no
+ expect($('[data-emoji=anger]').is(':visible')).toBe no
+ expect($('[data-emoji=alien]').is(':visible')).toBe yes
+
+
+ describe 'emoji menu', ->
+
+ selector = '[data-emoji=sunglasses]'
+
+ openEmojiMenuAndAddEmoji = ->
+
+ $('.js-add-award').eq(0).click()
+
+ $menu = $ '.emoji-menu'
+ $block = $ '.js-awards-block'
+ $emoji = $menu.find ".emoji-menu-list-item #{selector}"
+
+ expect($emoji.length).toBe 1
+ expect($block.find(selector).length).toBe 0
+
+ $emoji.click()
+
+ expect($menu.hasClass('.is-visible')).toBe no
+ expect($block.find(selector).length).toBe 1
+
+
+ it 'should add selected emoji to awards block', ->
+
+ openEmojiMenuAndAddEmoji()
+
+
+ it 'should remove already selected emoji', ->
+
+ openEmojiMenuAndAddEmoji()
+ $('.js-add-award').eq(0).click()
+
+ $block = $ '.js-awards-block'
+ $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}"
+
+ $emoji.click()
+ expect($block.find(selector).length).toBe 0
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
index 09708c12ed4..d3b003a328a 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee
+++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', ->
}
it 'does not respond to other keyCodes', ->
- $('input').trigger(keydownEvent(keyCode: 32))
+ $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to Enter alone', ->
- $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
+ $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to repeated events', ->
- $('input').trigger(keydownEvent(repeat: true))
+ $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
expect(@spies.submit).not.toHaveBeenTriggered()
@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', ->
# only run the tests that apply to the current platform
if navigator.userAgent.match(/Macintosh/)
it 'responds to Meta+Enter', ->
- $('input').trigger(keydownEvent())
+ $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', ->
- $('input').trigger(keydownEvent(altKey: true))
- $('input').trigger(keydownEvent(ctrlKey: true))
- $('input').trigger(keydownEvent(shiftKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered()
else
it 'responds to Ctrl+Enter', ->
- $('input').trigger(keydownEvent())
+ $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', ->
- $('input').trigger(keydownEvent(altKey: true))
- $('input').trigger(keydownEvent(metaKey: true))
- $('input').trigger(keydownEvent(shiftKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(metaKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered()
diff --git a/spec/javascripts/fixtures/application.html.haml b/spec/javascripts/fixtures/application.html.haml
new file mode 100644
index 00000000000..3fc6114407d
--- /dev/null
+++ b/spec/javascripts/fixtures/application.html.haml
@@ -0,0 +1,2 @@
+%a#test-link.btn.disabled{:href => "/foo"} Test link
+%button#test-button.btn.disabled Test Button
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
new file mode 100644
index 00000000000..d55936ee4f9
--- /dev/null
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -0,0 +1,52 @@
+.issue-details.issuable-details
+ .detail-page-description.content-block
+ %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
+ .description.js-task-list-container.is-task-list-enabled
+ .wiki
+ %p Qui exercitationem magnam optio quae fuga earum odio.
+ %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
+ %small.edited-text
+ .content-block.content-block-small
+ .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
+ %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
+ .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
+ %span.award-control-text.js-counter 0
+ %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
+ .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
+ %span.award-control-text.js-counter 0
+ .award-menu-holder.js-award-holder
+ %button.btn.award-control.js-add-award{:type => "button"}
+ %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
+ %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
+ %span.award-control-text Add
+ %section.issuable-discussion
+ #notes
+ %ul#notes-list.notes.main-notes-list.timeline
+ %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
+ .timeline-entry-inner
+ .timeline-icon
+ %a{:href => "/u/agustin"}
+ %img.avatar.s40{:alt => "", :src => "#"}/
+ .timeline-content
+ .note-header
+ %a.author_link{:href => "/u/agustin"}
+ %span.author Brenna Stokes
+ .inline.note-headline-light
+ @agustin commented
+ %a{:href => "#note_348"}
+ %time 11 days ago
+ .note-actions
+ %span.note-role Reporter
+ %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
+ %i.fa.fa-spinner.fa-spin
+ %i.fa.fa-smile-o
+ .js-task-list-container.note-body.is-task-list-enabled
+ .note-text
+ %p Suscipit sunt quia quisquam sed eveniet ipsam.
+ .note-awards
+ .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
+ .award-menu-holder.js-award-holder
+ %button.btn.award-control.js-add-award{:type => "button"}
+ %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
+ %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
+ %span.award-control-text Add
diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
index e3788bee813..dc2ceed42f4 100644
--- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
+++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
@@ -1,5 +1,5 @@
%form.js-quick-submit{ action: '/foo' }
- %input{ type: 'text' }
+ %input{ type: 'text', class: 'quick-submit-input'}
%textarea
%input{ type: 'submit'} Submit
diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee
new file mode 100644
index 00000000000..ce1a41390d2
--- /dev/null
+++ b/spec/javascripts/fixtures/emoji_menu.coffee
@@ -0,0 +1,957 @@
+window.emojiMenu = """
+ <div class='emoji-menu'>
+ <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" />
+ <div class='emoji-menu-content'>
+ <h5 class='emoji-menu-title'>
+ Emoticons
+ </h5>
+ <ul class='clearfix emoji-menu-list'>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F472" title="man_with_gua_pi_mao" data-aliases="" data-emoji="man_with_gua_pi_mao" data-unicode-name="1F472"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F471" title="person_with_blond_hair" data-aliases="" data-emoji="person_with_blond_hair" data-unicode-name="1F471"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64E" title="person_with_pouting_face" data-aliases="" data-emoji="person_with_pouting_face" data-unicode-name="1F64E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3BD" title="running_shirt_with_sash" data-aliases="" data-emoji="running_shirt_with_sash" data-unicode-name="1F3BD"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61D" title="stuck_out_tongue_closed_eyes" data-aliases="" data-emoji="stuck_out_tongue_closed_eyes" data-unicode-name="1F61D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61C" title="stuck_out_tongue_winking_eye" data-aliases="" data-emoji="stuck_out_tongue_winking_eye" data-unicode-name="1F61C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46C" title="two_men_holding_hands" data-aliases="" data-emoji="two_men_holding_hands" data-unicode-name="1F46C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46D" title="two_women_holding_hands" data-aliases="" data-emoji="two_women_holding_hands" data-unicode-name="1F46D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div>
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+"""
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
new file mode 100644
index 00000000000..95efaff4b69
--- /dev/null
+++ b/spec/javascripts/fixtures/right_sidebar.html.haml
@@ -0,0 +1,13 @@
+%div
+ %div.page-gutter.page-with-sidebar
+
+ %aside.right-sidebar
+ %div.block.issuable-sidebar-header
+ %a.gutter-toggle.pull-right.js-sidebar-toggle
+ %i.fa.fa-angle-double-left
+
+ %form.issuable-context-form
+ %div.block.labels
+ %div.sidebar-collapsed-icon
+ %i.fa.fa-tags
+ %span 1
diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml
new file mode 100644
index 00000000000..7785120da5b
--- /dev/null
+++ b/spec/javascripts/fixtures/search_autocomplete.html.haml
@@ -0,0 +1,10 @@
+.search.search-form.has-location-badge
+ %form.navbar-form
+ .search-input-container
+ %div.location-badge
+ This project
+ .search-input-wrap
+ .dropdown
+ %input#search.search-input.dropdown-menu-toggle
+ .dropdown-menu.dropdown-select
+ .dropdown-content
diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml
new file mode 100644
index 00000000000..859e79a6c9e
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml
@@ -0,0 +1 @@
+= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml
new file mode 100644
index 00000000000..5ed51be689c
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f/register.html.haml
@@ -0,0 +1,2 @@
+- user = FactoryGirl.build(:user, :two_factor_via_otp)
+= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user }
diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 78d39f1b428..82ee1954a59 100644
--- a/spec/javascripts/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph_contributors_graph
+//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index dbafe782b77..56970e22e34 100644
--- a/spec/javascripts/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph_contributors_util
+//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
@@ -9,14 +9,14 @@ describe("ContributorsStatGraphUtil", function () {
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}]
-
+
var correct_parsed_log = {
total: [
{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
by_author:
[
- {
+ {
author_name: "Karlo Soriano", author_email: "karlo@email.com",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
@@ -132,8 +132,8 @@ describe("ContributorsStatGraphUtil", function () {
total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
by_author:[
- {
- author: "Karlo Soriano",
+ {
+ author: "Karlo Soriano",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
{
@@ -161,11 +161,11 @@ describe("ContributorsStatGraphUtil", function () {
it("returns the log by author sorted by specified field", function () {
var fake_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
+ {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
],
by_author: [
- {
+ {
author_name: "Karlo Soriano", author_email: "karlo@email.com",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 4c652910cd6..4b05d401a42 100644
--- a/spec/javascripts/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph
+//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index ea27f36e9b5..d84d80f266b 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -1,3 +1,4 @@
+#= require lib/utils/text_utility
#= require issue
describe 'Issue', ->
@@ -38,7 +39,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeVisible()
expect($btnClose).toBeHidden()
expect($('div.status-box-closed')).toBeVisible()
@@ -50,7 +51,7 @@ describe 'reopen/close issue', ->
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')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -59,7 +60,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -73,7 +74,7 @@ describe 'reopen/close issue', ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://goesnowhere.nothing/whereami')
req.error()
-
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -82,7 +83,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -105,4 +106,4 @@ describe 'reopen/close issue', ->
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-open')).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file
+ expect($('div.status-box-closed')).toBeHidden()
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
index 22ebc7039d1..3cb67d51c85 100644
--- a/spec/javascripts/merge_request_spec.js.coffee
+++ b/spec/javascripts/merge_request_spec.js.coffee
@@ -6,7 +6,7 @@ describe 'MergeRequest', ->
beforeEach ->
fixture.load('merge_requests_show.html')
- @merge = new MergeRequest({})
+ @merge = new MergeRequest()
it 'modifies the Markdown field', ->
spyOn(jQuery, 'ajax').and.stub()
diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee
index f2ce85efcdc..ce773793817 100644
--- a/spec/javascripts/new_branch_spec.js.coffee
+++ b/spec/javascripts/new_branch_spec.js.coffee
@@ -1,4 +1,4 @@
-#= require jquery-ui
+#= require jquery-ui/autocomplete
#= require new_branch_form
describe 'Branch', ->
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
index dd160e821b3..3a3c8d63e82 100644
--- a/spec/javascripts/notes_spec.js.coffee
+++ b/spec/javascripts/notes_spec.js.coffee
@@ -1,7 +1,7 @@
#= require notes
#= require gl_form
-window.gon = {}
+window.gon or= {}
window.disableButtonIfEmptyField = -> null
describe 'Notes', ->
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
index 3d8de2ff989..f0d26fb5446 100644
--- a/spec/javascripts/project_title_spec.js.coffee
+++ b/spec/javascripts/project_title_spec.js.coffee
@@ -1,11 +1,12 @@
#= require bootstrap
#= require select2
+#= require lib/utils/type_utility
#= require gl_dropdown
#= require api
#= require project_select
#= require project
-window.gon = {}
+window.gon or= {}
window.gon.api_version = 'v3'
describe 'Project Title', ->
diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee
new file mode 100644
index 00000000000..2075cacdb67
--- /dev/null
+++ b/spec/javascripts/right_sidebar_spec.js.coffee
@@ -0,0 +1,69 @@
+#= require right_sidebar
+#= require jquery
+#= require jquery.cookie
+
+@sidebar = null
+$aside = null
+$toggle = null
+$icon = null
+$page = null
+$labelsIcon = null
+
+
+assertSidebarState = (state) ->
+
+ shouldBeExpanded = state is 'expanded'
+ shouldBeCollapsed = state is 'collapsed'
+
+ expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
+ expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
+ expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded
+
+ expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
+ expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
+ expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed
+
+
+describe 'RightSidebar', ->
+
+ fixture.preload 'right_sidebar.html'
+
+ beforeEach ->
+ fixture.load 'right_sidebar.html'
+
+ @sidebar = new Sidebar
+ $aside = $ '.right-sidebar'
+ $page = $ '.page-with-sidebar'
+ $icon = $aside.find 'i'
+ $toggle = $aside.find '.js-sidebar-toggle'
+ $labelsIcon = $aside.find '.sidebar-collapsed-icon'
+
+
+ it 'should expand the sidebar when arrow is clicked', ->
+
+ $toggle.click()
+ assertSidebarState 'expanded'
+
+
+ it 'should collapse the sidebar when arrow is clicked', ->
+
+ $toggle.click()
+ assertSidebarState 'expanded'
+
+ $toggle.click()
+ assertSidebarState 'collapsed'
+
+
+ it 'should float over the page and when sidebar icons clicked', ->
+
+ $labelsIcon.click()
+ assertSidebarState 'expanded'
+
+
+ it 'should collapse when the icon arrow clicked while it is floating on page', ->
+
+ $labelsIcon.click()
+ assertSidebarState 'expanded'
+
+ $toggle.click()
+ assertSidebarState 'collapsed'
diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee
new file mode 100644
index 00000000000..1c1faca3333
--- /dev/null
+++ b/spec/javascripts/search_autocomplete_spec.js.coffee
@@ -0,0 +1,149 @@
+#= require gl_dropdown
+#= require search_autocomplete
+#= require jquery
+#= require lib/utils/common_utils
+#= require lib/utils/type_utility
+#= require fuzzaldrin-plus
+
+
+widget = null
+userId = 1
+window.gon or= {}
+window.gon.current_user_id = userId
+
+dashboardIssuesPath = '/dashboard/issues'
+dashboardMRsPath = '/dashboard/merge_requests'
+projectIssuesPath = '/gitlab-org/gitlab-ce/issues'
+projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'
+groupIssuesPath = '/groups/gitlab-org/issues'
+groupMRsPath = '/groups/gitlab-org/merge_requests'
+projectName = 'GitLab Community Edition'
+groupName = 'Gitlab Org'
+
+
+# Add required attributes to body before starting the test.
+# section would be dashboard|group|project
+addBodyAttributes = (section = 'dashboard') ->
+
+ $body = $ 'body'
+
+ $body.removeAttr 'data-page'
+ $body.removeAttr 'data-project'
+ $body.removeAttr 'data-group'
+
+ switch section
+ when 'dashboard'
+ $body.data 'page', 'root:index'
+ when 'group'
+ $body.data 'page', 'groups:show'
+ $body.data 'group', 'gitlab-org'
+ when 'project'
+ $body.data 'page', 'projects:show'
+ $body.data 'project', 'gitlab-ce'
+
+
+# Mock `gl` object in window for dashboard specific page. App code will need it.
+mockDashboardOptions = ->
+
+ window.gl or= {}
+ window.gl.dashboardOptions =
+ issuesPath: dashboardIssuesPath
+ mrPath : dashboardMRsPath
+
+
+# Mock `gl` object in window for project specific page. App code will need it.
+mockProjectOptions = ->
+
+ window.gl or= {}
+ window.gl.projectOptions =
+ 'gitlab-ce' :
+ issuesPath : projectIssuesPath
+ mrPath : projectMRsPath
+ projectName : projectName
+
+
+mockGroupOptions = ->
+
+ window.gl or= {}
+ window.gl.groupOptions =
+ 'gitlab-org' :
+ issuesPath : groupIssuesPath
+ mrPath : groupMRsPath
+ projectName : groupName
+
+
+assertLinks = (list, issuesPath, mrsPath) ->
+
+ issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}"
+ issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}"
+ mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}"
+ mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}"
+
+ a1 = "a[href='#{issuesAssignedToMeLink}']"
+ a2 = "a[href='#{issuesIHaveCreatedLink}']"
+ a3 = "a[href='#{mrsAssignedToMeLink}']"
+ a4 = "a[href='#{mrsIHaveCreatedLink}']"
+
+ expect(list.find(a1).length).toBe 1
+ expect(list.find(a1).text()).toBe ' Issues assigned to me '
+
+ expect(list.find(a2).length).toBe 1
+ expect(list.find(a2).text()).toBe " Issues I've created "
+
+ expect(list.find(a3).length).toBe 1
+ expect(list.find(a3).text()).toBe ' Merge requests assigned to me '
+
+ expect(list.find(a4).length).toBe 1
+ expect(list.find(a4).text()).toBe " Merge requests I've created "
+
+
+describe 'Search autocomplete dropdown', ->
+
+ fixture.preload 'search_autocomplete.html'
+
+ beforeEach ->
+
+ fixture.load 'search_autocomplete.html'
+ widget = new SearchAutocomplete
+
+
+ it 'should show Dashboard specific dropdown menu', ->
+
+ addBodyAttributes()
+ mockDashboardOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, dashboardIssuesPath, dashboardMRsPath
+
+
+ it 'should show Group specific dropdown menu', ->
+
+ addBodyAttributes 'group'
+ mockGroupOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, groupIssuesPath, groupMRsPath
+
+
+ it 'should show Project specific dropdown menu', ->
+
+ addBodyAttributes 'project'
+ mockProjectOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, projectIssuesPath, projectMRsPath
+
+
+ it 'should not show category related menu if there is text in the input', ->
+
+ addBodyAttributes 'project'
+ mockProjectOptions()
+ widget.searchInput.val 'help'
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']"
+ expect(list.find(link).length).toBe 0
diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee
new file mode 100644
index 00000000000..e8a2892d678
--- /dev/null
+++ b/spec/javascripts/u2f/authenticate_spec.coffee
@@ -0,0 +1,52 @@
+#= require u2f/authenticate
+#= require u2f/util
+#= require u2f/error
+#= require u2f
+#= require ./mock_u2f_device
+
+describe 'U2FAuthenticate', ->
+ U2FUtil.enableTestMode()
+ fixture.load('u2f/authenticate')
+
+ beforeEach ->
+ @u2fDevice = new MockU2FDevice
+ @container = $("#js-authenticate-u2f")
+ @component = new U2FAuthenticate(@container, {}, "token")
+ @component.start()
+
+ it 'allows authenticating via a U2F device', ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupMessage = @container.find("p")
+ expect(setupMessage.text()).toContain('Insert your security key')
+ expect(setupButton.text()).toBe('Login Via U2F Device')
+ setupButton.trigger('click')
+
+ inProgressMessage = @container.find("p")
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
+
+ @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
+ authenticatedMessage = @container.find("p")
+ deviceResponse = @container.find('#js-device-response')
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
+ expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
+
+ describe "errors", ->
+ it "displays an error message", ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("There was a problem communicating with your device")
+
+ it "allows retrying authentication after an error", ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
+ retryButton = @container.find("#js-u2f-try-again")
+ retryButton.trigger('click')
+
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
+ authenticatedMessage = @container.find("p")
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee
new file mode 100644
index 00000000000..97ed0e83a0e
--- /dev/null
+++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee
@@ -0,0 +1,15 @@
+class @MockU2FDevice
+ constructor: () ->
+ window.u2f ||= {}
+
+ window.u2f.register = (appId, registerRequests, signRequests, callback) =>
+ @registerCallback = callback
+
+ window.u2f.sign = (appId, challenges, signRequests, callback) =>
+ @authenticateCallback = callback
+
+ respondToRegisterRequest: (params) =>
+ @registerCallback(params)
+
+ respondToAuthenticateRequest: (params) =>
+ @authenticateCallback(params)
diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee
new file mode 100644
index 00000000000..0858abeca1a
--- /dev/null
+++ b/spec/javascripts/u2f/register_spec.js.coffee
@@ -0,0 +1,57 @@
+#= require u2f/register
+#= require u2f/util
+#= require u2f/error
+#= require u2f
+#= require ./mock_u2f_device
+
+describe 'U2FRegister', ->
+ U2FUtil.enableTestMode()
+ fixture.load('u2f/register')
+
+ beforeEach ->
+ @u2fDevice = new MockU2FDevice
+ @container = $("#js-register-u2f")
+ @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token")
+ @component.start()
+
+ it 'allows registering a U2F device', ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ expect(setupButton.text()).toBe('Setup New U2F Device')
+ setupButton.trigger('click')
+
+ inProgressMessage = @container.children("p")
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
+
+ @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
+ registeredMessage = @container.find('p')
+ deviceResponse = @container.find('#js-device-response')
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!")
+ expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
+
+ describe "errors", ->
+ it "doesn't allow the same device to be registered twice (for the same user", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: 4})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("already been registered with us")
+
+ it "displays an error message for other errors", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("There was a problem communicating with your device")
+
+ it "allows retrying registration after an error", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
+ retryButton = @container.find("#U2FTryAgain")
+ retryButton.trigger('click')
+
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
+ registeredMessage = @container.find("p")
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!")
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb
deleted file mode 100644
index 88c22912950..00000000000
--- a/spec/lib/award_emoji_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe AwardEmoji do
- describe '.urls' do
- subject { AwardEmoji.urls }
-
- it { is_expected.to be_an_instance_of(Array) }
- it { is_expected.to_not be_empty }
-
- context 'every Hash in the Array' do
- it 'has the correct keys and values' do
- subject.each do |hash|
- expect(hash[:name]).to be_an_instance_of(String)
- expect(hash[:path]).to be_an_instance_of(String)
- end
- end
- end
- end
-
- describe '.emoji_by_category' do
- it "only contains known categories" do
- undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys
- expect(undefined_categories).to be_empty
- end
- end
-end
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
new file mode 100644
index 00000000000..1ee31a603e4
--- /dev/null
+++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbstractReferenceFilter do
+ let(:project) { create(:empty_project) }
+
+ describe '#references_per_project' do
+ it 'returns a Hash containing references grouped per project paths' do
+ doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
+ expect(filter).to receive(:object_sym).twice.and_return(:issue)
+
+ refs = filter.references_per_project
+
+ expect(refs).to be_an_instance_of(Hash)
+ expect(refs[project.to_reference]).to eq(Set.new(%w[1 2]))
+ end
+ end
+
+ describe '#projects_per_reference' do
+ it 'returns a Hash containing projects grouped per project paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new(%w[1]) })
+
+ expect(filter.projects_per_reference).
+ to eq({ project.path_with_namespace => project })
+ end
+ end
+
+ describe '#find_projects_for_paths' do
+ it 'returns a list of Projects for a list of paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.find_projects_for_paths([project.path_with_namespace])).
+ to eq([project])
+ end
+ end
+
+ describe '#current_project_path' do
+ it 'returns the path of the current project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.current_project_path).to eq(project.path_with_namespace)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index c2a8ad36c30..593bd6d5cac 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
context 'cross-project reference' do
@@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
context 'cross-project URL reference' do
@@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
end
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 63a32d9d455..d46d3f1489e 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
context 'cross-project reference' do
@@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
exp = act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
context 'cross-project URL reference' do
@@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index f4c5c621bd0..695a5bc6fd4 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -19,19 +19,31 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
- it 'adds rel="nofollow" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
-
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ context 'for root links on document' do
+ let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
- it 'adds rel="noreferrer" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
+ context 'for nested links on document' do
+ let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
end
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index dd5594750c8..a2a1ed58d1b 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -21,4 +21,9 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
doc = filter(image('https://i.imgur.com/DfssX9C.jpg'))
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end
+
+ it 'wraps the image with a link and a div' do
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.to_html).to include('<div class="image-container">')
+ end
end
diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
new file mode 100644
index 00000000000..9e526371294
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Banzai::Filter::InlineDiffFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'adds inline diff span tags for deletions when using square brackets' do
+ doc = "START [-something deleted-] END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+ end
+
+ it 'adds inline diff span tags for deletions when using curley braces' do
+ doc = "START {-something deleted-} END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+ end
+
+ it 'does not add inline diff span tags when a closing tag is not provided' do
+ doc = "START [- END"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'adds inline span tags for additions when using square brackets' do
+ doc = "START [+something added+] END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+ end
+
+ it 'adds inline span tags for additions when using curley braces' do
+ doc = "START {+something added+} END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+ end
+
+ it 'does not add inline diff span tags when a closing addition tag is not provided' do
+ doc = "START {+ END"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'does not add inline diff span tags when the tags do not match' do
+ examples = [
+ "{+ additions +]",
+ "[+ additions +}",
+ "{- delletions -]",
+ "[- delletions -}"
+ ]
+
+ examples.each do |doc|
+ expect(filter(doc).to_html).to eq(doc)
+ end
+ end
+
+ it 'prevents user-land html being injected' do
+ doc = "START {+&lt;script&gt;alert('I steal cookies')&lt;/script&gt;+} END"
+ expect(filter(doc).to_html).to eq("START <span class=\"idiff left right addition\">&lt;script&gt;alert('I steal cookies')&lt;/script&gt;</span> END")
+ end
+
+ it 'preserves content inside pre tags' do
+ doc = "<pre>START {+something added+} END</pre>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'preserves content inside code tags' do
+ doc = "<code>START {+something added+} END</code>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'preserves content inside tt tags' do
+ doc = "<tt>START {+something added+} END</tt>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 266ebef33d6..a005b4990e7 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -25,7 +25,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
- expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -91,11 +93,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
-
it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>")
@@ -112,8 +109,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference(project) }
it 'ignores valid references when cross-reference project uses external tracker' do
- expect_any_instance_of(Project).to receive(:get_issue).
- with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project2, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -137,9 +135,10 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
+ it 'ignores out-of-bounds issue IDs on the referenced project' do
+ exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
end
end
@@ -160,11 +159,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
context 'cross-project reference in link href' do
@@ -184,11 +178,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
context 'cross-project URL in link href' do
@@ -208,10 +197,54 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
+ end
+
+ context 'referencing external issues' do
+ let(:project) { create(:redmine_project) }
+
+ it 'renders internal issue IDs as external issue links' do
+ doc = reference_filter('#1')
+ link = doc.css('a').first
+
+ expect(link.attr('data-reference-type')).to eq('external_issue')
+ expect(link.attr('title')).to eq('Issue in Redmine')
+ expect(link.attr('data-external-issue')).to eq('1')
+ end
+ end
+
+ describe '#issues_per_Project' do
+ context 'using an internal issue tracker' do
+ it 'returns a Hash containing the issues per project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:projects_per_reference).
+ and_return({ project.path_with_namespace => project })
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new([issue.iid]) })
+
+ expect(filter.issues_per_project).
+ to eq({ project => { issue.iid => issue } })
+ end
+ end
+
+ context 'using an external issue tracker' do
+ it 'returns a Hash containing the issues per project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(project).to receive(:default_issues_tracker?).and_return(false)
+
+ expect(filter).to receive(:projects_per_reference).
+ and_return({ project.path_with_namespace => project })
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new([1]) })
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
+ expect(filter.issues_per_project[project][1]).
+ to be_an_instance_of(ExternalIssue)
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index b0a38e7c251..9e3d2f5825d 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Label #{reference}")
- expect(result[:references][:label]).to eq [label]
- end
-
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
@@ -109,6 +104,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based single-word references with special characters' do
+ let(:label) { create(:label, name: '?gfm&', project: project) }
+ let(:reference) { "#{Label.reference_prefix}#{label.name}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See ?gfm&'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>\?gfm&amp;</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ act = "Label #{Label.reference_prefix}#{label.name.reverse}"
+ exp = "Label #{Label.reference_prefix}&amp;mfg?"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) }
let(:reference) { label.to_reference(format: :name) }
@@ -133,6 +153,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based multi-word references with special characters in quotes' do
+ let(:label) { create(:label, name: 'gfm & references?', project: project) }
+ let(:reference) { label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See gfm & references?'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>gfm &amp; references\?</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
+ exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mfg\")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
describe 'edge cases' do
it 'gracefully handles non-references matching the pattern' do
exp = act = '(format nil "~0f" 3.0) ; 3.0'
@@ -170,11 +215,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to have_attribute('data-label')
expect(link.attr('data-label')).to eq label.id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Label #{reference}")
- expect(result[:references][:label]).to eq [label]
- end
end
describe 'cross project label references' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 352710df307..805acf1c8b3 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -38,6 +38,12 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+ it 'ignores out-of-bounds merge request IDs on the referenced project' do
+ exp = act = "Merge !#{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
it 'includes a title attribute' do
doc = reference_filter("Merge #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
@@ -78,11 +84,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
end
context 'cross-project reference' do
@@ -109,11 +110,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
end
context 'cross-project URL reference' do
@@ -133,10 +129,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
doc = reference_filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- 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
index 5beb61dac5c..9424f2363e1 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:reference) { milestone.to_reference }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
@@ -17,11 +18,37 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
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 '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 'supports an :only_path context' do
+ doc = reference_filter("Milestone #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.
+ namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ context 'Integer-based references' do
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -30,29 +57,82 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
end
it 'links with adjacent text' do
- doc = reference_filter("milestone (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/)
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</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}"
+ it 'ignores invalid milestone IIDs' do
+ exp = act = "Milestone #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
end
+ end
+
+ context 'String-based single-word references' do
+ let(:milestone) { create(:milestone, name: 'gfm', project: project) }
+ let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'escapes the title attribute' do
- milestone.update_attribute(:title, %{"></a>whatever<a title="})
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ expect(doc.text).to eq 'See gfm'
+ end
- doc = reference_filter("milestone #{reference}")
- expect(doc.text).to eq "milestone \">whatever"
+ it 'links with adjacent text' do
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\)))
end
- it 'includes default classes' do
- doc = reference_filter("milestone #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ it 'ignores invalid milestone names' do
+ exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'String-based multi-word references in quotes' do
+ let(:milestone) { create(:milestone, name: 'gfm references', project: project) }
+ let(:reference) { milestone.to_reference(format: :name) }
+
+ 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)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\)))
+ end
+
+ it 'ignores invalid milestone names' do
+ exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'referencing a milestone in a link href' do
+ let(:reference) { %Q{<a href="#{milestone.to_reference}">Milestone</a>} }
+
+ 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(%r(\(<a.+>Milestone</a>\.\)))
end
it 'includes a data-project attribute' do
- doc = reference_filter("milestone #{reference}")
+ doc = reference_filter("Milestone #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
@@ -66,10 +146,31 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
expect(link).to have_attribute('data-milestone')
expect(link.attr('data-milestone')).to eq milestone.id.to_s
end
+ end
+
+ describe 'cross project milestone references' do
+ let(:another_project) { create(:empty_project, :public) }
+ let(:project_path) { another_project.path_with_namespace }
+ let(:milestone) { create(:milestone, project: another_project) }
+ let(:reference) { milestone.to_reference(project) }
+
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'points to referenced project milestone page' do
+ expect(result.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(another_project.namespace,
+ another_project,
+ milestone)
+ end
- it 'adds to the results hash' do
- result = reference_pipeline_result("milestone #{reference}")
- expect(result[:references][:milestone]).to eq [milestone]
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+ end
+
+ it 'escapes the name attribute' do
+ allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
end
end
end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index c2c2fd0eb6a..f181125156b 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
context 'with data-project' do
+ let(:parser_class) do
+ Class.new(Banzai::ReferenceParser::BaseParser) do
+ self.reference_type = :test
+ end
+ end
+
+ before do
+ allow(Banzai::ReferenceParser).to receive(:[]).
+ with('test').
+ and_return(parser_class)
+ end
+
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
- link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
@@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project)
project.team << [user, :master]
- link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Project references' do
- link = reference_link(project: 12345, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error
end
@@ -51,18 +63,30 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: non_member)
expect(doc.css('a').length).to eq 0
end
+ it 'removes references for project members with guest role' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :guest]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, current_user: 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')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: author)
expect(doc.css('a').length).to eq 1
@@ -73,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
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')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: assignee)
expect(doc.css('a').length).to eq 1
@@ -85,7 +109,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project.team << [member, :developer]
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: member)
expect(doc.css('a').length).to eq 1
@@ -96,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: admin)
expect(doc.css('a').length).to eq 1
@@ -108,7 +132,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
@@ -121,7 +145,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
user = create(:user)
group = create(:group, :private)
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
@@ -132,14 +156,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
group = create(:group, :private)
group.add_developer(user)
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Group references' do
- link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: 12345, reference_type: 'user')
expect { filter(link) }.not_to raise_error
end
@@ -149,7 +173,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows any User reference' do
user = create(:user)
- link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(user: user.id, reference_type: 'user')
doc = filter(link)
expect(doc.css('a').length).to eq 1
diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb
new file mode 100644
index 00000000000..55e681f6faf
--- /dev/null
+++ b/spec/lib/banzai/filter/reference_filter_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ReferenceFilter, lib: true do
+ let(:project) { build(:project) }
+
+ describe '#each_node' do
+ it 'iterates over the nodes in a document' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.
+ to yield_with_args(an_instance_of(Nokogiri::XML::Element))
+ end
+
+ it 'returns an Enumerator when no block is given' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect(filter.each_node).to be_an_instance_of(Enumerator)
+ end
+
+ it 'skips links with a "gfm" class' do
+ document = Nokogiri::HTML.fragment('<a href="foo" class="gfm">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.not_to yield_control
+ end
+
+ it 'skips text nodes in pre elements' do
+ document = Nokogiri::HTML.fragment('<pre>foo</pre>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.not_to yield_control
+ end
+ end
+
+ describe '#nodes' do
+ it 'returns an Array of the HTML nodes' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect(filter.nodes).to eq([document.children[0]])
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb b/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb
deleted file mode 100644
index c8b1dfdf944..00000000000
--- a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::ReferenceGathererFilter, lib: true do
- include ActionView::Helpers::UrlHelper
- include FilterSpecHelper
-
- def reference_link(data)
- link_to('text', '', class: 'gfm', data: data)
- end
-
- context "for issue references" do
-
- context 'with data-project' do
- it 'removes unpermitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- issue = create(:issue, project: project)
-
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:issue]).to be_empty
- end
-
- it 'allows permitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- issue = create(:issue, project: project)
- project.team << [user, :master]
-
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:issue]).to eq([issue])
- end
-
- it 'handles invalid Project references' do
- link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter')
-
- expect { pipeline_result(link) }.not_to raise_error
- end
- end
- end
-
- context "for user references" do
-
- context 'with data-group' do
- it 'removes unpermitted Group references' do
- user = create(:user)
- group = create(:group)
-
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:user]).to be_empty
- end
-
- it 'allows permitted Group references' do
- user = create(:user)
- group = create(:group)
- group.add_developer(user)
-
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:user]).to eq([user])
- end
-
- it 'handles invalid Group references' do
- link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
-
- expect { pipeline_result(link) }.not_to raise_error
- end
- end
-
- context 'with data-user' do
- it 'allows any User reference' do
- user = create(:user)
-
- link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link)
-
- expect(result[:references][:user]).to eq([user])
- end
- end
- end
-end
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0e6685f0ffb..b9e4a4eaf0e 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -132,11 +132,8 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
path = 'files/images/한글.png'
escaped = Addressable::URI.escape(path)
- # Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class).
- to receive(:file_exists?).and_return(true)
- allow_any_instance_of(described_class).
- to receive(:image?).with(path).and_return(true)
+ # Stub this method so the file doesn't actually need to be in the repo
+ allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match '/raw/'
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 26466fbb180..5068ddd7faa 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
context 'cross-project reference' do
@@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
context 'cross-project URL reference' do
@@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
end
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index b83be54746c..273d2ed709a 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -23,6 +23,14 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
%(<a href="#{path}">#{path}</a>)
end
+ def nested_image(path)
+ %(<div><img src="#{path}" /></div>)
+ end
+
+ def nested_link(path)
+ %(<div><a href="#{path}">#{path}</a></div>)
+ end
+
let(:project) { create(:project) }
shared_examples :preserve_unchanged do
@@ -47,11 +55,19 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('a')['href']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('a')['href']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
it 'rebuilds relative URL for an image' do
- doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('a')['href']).
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 8bdebae1841..108b36a97cc 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
it 'supports a special @all mention' do
- doc = reference_filter("Hey #{reference}")
+ doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
- context "when the author is a member of the project" do
+ it 'includes a data-author attribute when there is an author' do
+ doc = reference_filter(reference, author: user)
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}", author: project.creator)
- expect(result[:references][:user]).to eq [project.creator]
- end
+ expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
end
- context "when the author is not a member of the project" do
-
- let(:other_user) { create(:user) }
+ it 'does not include a data-author attribute when there is no author' do
+ doc = reference_filter(reference)
- it "doesn't add to the results hash" do
- result = reference_pipeline_result("Hey #{reference}", author: other_user)
- expect(result[:references][:user]).to eq []
- end
+ expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end
end
@@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [user]
- end
end
context 'mentioning a group' do
@@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-group')
expect(link.attr('data-group')).to eq group.id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq group.users
- end
end
it 'links with adjacent text' do
@@ -151,10 +135,24 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
+ end
+
+ describe '#namespaces' do
+ it 'returns a Hash containing all Namespaces' do
+ document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
+ filter = described_class.new(document, project: project)
+ ns = user.namespace
+
+ expect(filter.namespaces).to eq({ ns.path => ns })
+ end
+ end
+
+ describe '#usernames' do
+ it 'returns the usernames mentioned in a document' do
+ document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
+ filter = described_class.new(document, project: project)
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [user]
+ expect(filter.usernames).to eq([user.username])
end
end
end
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
new file mode 100644
index 00000000000..92d88c4172c
--- /dev/null
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Banzai::Filter::WikiLinkFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
+ let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:user) { double }
+ let(:wiki) { ProjectWiki.new(project, user) }
+
+ it "doesn't rewrite absolute links" do
+ filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq('http://example.com:8000/')
+ end
+
+ describe "invalid links" do
+ invalid_links = ["http://:8080", "http://", "http://:8080/path"]
+
+ invalid_links.each do |invalid_link|
+ it "doesn't rewrite invalid invalid_links like #{invalid_link}" do
+ filtered_link = filter("<a href='#{invalid_link}'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq(invalid_link)
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
new file mode 100644
index 00000000000..98f76f36fd5
--- /dev/null
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Banzai::NoteRenderer do
+ describe '.render' do
+ it 'renders a Note' do
+ note = double(:note)
+ project = double(:project)
+ wiki = double(:wiki)
+ user = double(:user)
+
+ expect(Banzai::ObjectRenderer).to receive(:new).
+ with(project, user,
+ requested_path: 'foo',
+ project_wiki: wiki,
+ ref: 'bar',
+ pipeline: :note).
+ and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer).
+ to receive(:render).with([note], :note)
+
+ described_class.render([note], project, user, 'foo', wiki, 'bar')
+ end
+ end
+end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
new file mode 100644
index 00000000000..44256b32bdc
--- /dev/null
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::ObjectRenderer do
+ let(:project) { create(:empty_project) }
+ let(:user) { project.owner }
+
+ describe '#render' do
+ it 'renders and redacts an Array of objects' do
+ renderer = described_class.new(project, user)
+ object = double(:object, note: 'hello', note_html: nil)
+
+ expect(renderer).to receive(:render_objects).with([object], :note).
+ and_call_original
+
+ expect(renderer).to receive(:redact_documents).
+ with(an_instance_of(Array)).
+ and_call_original
+
+ expect(object).to receive(:note_html=).with('<p>hello</p>')
+
+ renderer.render([object], :note)
+ end
+ end
+
+ describe '#render_objects' do
+ it 'renders an Array of objects' do
+ object = double(:object, note: 'hello')
+ renderer = described_class.new(project, user)
+
+ expect(renderer).to receive(:render_attribute).with(object, :note).
+ and_call_original
+
+ rendered = renderer.render_objects([object], :note)
+
+ expect(rendered).to be_an_instance_of(Array)
+ expect(rendered[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ end
+ end
+
+ describe '#redact_documents' do
+ it 'redacts a set of documents and returns them as an Array of Strings' do
+ doc = Nokogiri::HTML.fragment('<p>hello</p>')
+ renderer = described_class.new(project, user)
+
+ expect_any_instance_of(Banzai::Redactor).to receive(:redact).
+ with([doc]).
+ and_call_original
+
+ redacted = renderer.redact_documents([doc])
+
+ expect(redacted).to eq(['<p>hello</p>'])
+ end
+ end
+
+ describe '#context_for' do
+ let(:object) { double(:object, note: 'hello') }
+ let(:renderer) { described_class.new(project, user) }
+
+ it 'returns a Hash' do
+ expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the cache key' do
+ context = renderer.context_for(object, :note)
+
+ expect(context[:cache_key]).to eq([object, :note])
+ end
+
+ context 'when the object responds to "author"' do
+ it 'includes the author in the context' do
+ expect(object).to receive(:author).and_return('Alice')
+
+ context = renderer.context_for(object, :note)
+
+ expect(context[:author]).to eq('Alice')
+ end
+ end
+
+ context 'when the object does not respond to "author"' do
+ it 'does not include the author in the context' do
+ context = renderer.context_for(object, :note)
+
+ expect(context.key?(:author)).to eq(false)
+ end
+ end
+ end
+
+ describe '#render_attribute' do
+ it 'renders the attribute of an object' do
+ object = double(:doc, note: 'hello')
+ renderer = described_class.new(project, user, pipeline: :note)
+ doc = renderer.render_attribute(object, :note)
+
+ expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ expect(doc.to_html).to eq('<p>hello</p>')
+ end
+ end
+
+ describe '#base_context' do
+ let(:context) do
+ described_class.new(project, user, pipeline: :note).base_context
+ end
+
+ it 'returns a Hash' do
+ expect(context).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the custom attributes' do
+ expect(context[:pipeline]).to eq(:note)
+ end
+
+ it 'includes the current user' do
+ expect(context[:current_user]).to eq(user)
+ end
+
+ it 'includes the current project' do
+ expect(context[:project]).to eq(project)
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 7aa1b4a3bf6..51c89ac4889 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -50,4 +50,111 @@ describe Banzai::Pipeline::WikiPipeline do
end
end
end
+
+ describe "Links" do
+ let(:namespace) { create(:namespace, name: "wiki_link_ns") }
+ let(:project) { create(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
+ let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+
+ { "when GitLab is hosted at a root URL" => '/',
+ "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root|
+ context test_name do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root)
+ end
+
+ describe "linking to pages within the wiki" do
+ context "when creating hierarchical links to the current directory" do
+ it "rewrites non-file links to be at the scope of the current directory" do
+ markdown = "[Page](./page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the current directory" do
+ markdown = "[Link to Page](./page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ end
+ end
+
+ context "when creating hierarchical links to the parent directory" do
+ it "rewrites non-file links to be at the scope of the parent directory" do
+ markdown = "[Link to Page](../page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the parent directory" do
+ markdown = "[Link to Page](../page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"")
+ end
+ end
+
+ context "when creating hierarchical links to a sub-directory" do
+ it "rewrites non-file links to be at the scope of the sub-directory" do
+ markdown = "[Link to Page](./subdirectory/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the sub-directory" do
+ markdown = "[Link to Page](./subdirectory/page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"")
+ end
+ end
+
+ describe "when creating non-hierarchical links" do
+ it 'rewrites non-file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the current directory" do
+ markdown = "[Link to Page](page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ end
+ end
+
+ describe "when creating root links" do
+ it 'rewrites non-file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ end
+
+ it 'rewrites file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](/page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"")
+ end
+ end
+ end
+
+ describe "linking to pages outside the wiki (absolute)" do
+ it "doesn't rewrite links" do
+ markdown = "[Link to Page](http://example.com/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include('href="http://example.com/page"')
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
new file mode 100644
index 00000000000..488f465bcda
--- /dev/null
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Banzai::Redactor do
+ let(:user) { build(:user) }
+ let(:project) { build(:empty_project) }
+ let(:redactor) { described_class.new(project, user) }
+
+ describe '#redact' do
+ it 'redacts an Array of documents' do
+ doc1 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+
+ doc2 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+
+ expect(redactor).to receive(:nodes_visible_to_user).and_return([])
+
+ expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2])
+
+ expect(doc1.to_html).to eq('foo')
+ expect(doc2.to_html).to eq('bar')
+ end
+ end
+
+ describe '#redact_nodes' do
+ it 'redacts an Array of nodes' do
+ doc = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ node = doc.children[0]
+
+ expect(redactor).to receive(:nodes_visible_to_user).
+ with([node]).
+ and_return(Set.new)
+
+ redactor.redact_nodes([node])
+
+ expect(doc.to_html).to eq('foo')
+ end
+ end
+
+ describe '#nodes_visible_to_user' do
+ it 'returns a Set containing the visible nodes' do
+ doc = Nokogiri::HTML.fragment('<a data-reference-type="issue"></a>')
+ node = doc.children[0]
+
+ expect_any_instance_of(Banzai::ReferenceParser::IssueParser).
+ to receive(:nodes_visible_to_user).
+ with(user, [node]).
+ and_return([node])
+
+ expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node]))
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
new file mode 100644
index 00000000000..543b4786d84
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -0,0 +1,237 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::BaseParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ subject do
+ klass = Class.new(described_class) do
+ self.reference_type = :foo
+ end
+
+ klass.new(project, user)
+ end
+
+ describe '.reference_type=' do
+ it 'sets the reference type' do
+ dummy = Class.new(described_class)
+ dummy.reference_type = :foo
+
+ expect(dummy.reference_type).to eq(:foo)
+ end
+ end
+
+ describe '#nodes_visible_to_user' do
+ let(:link) { empty_html_link }
+
+ context 'when the link has a data-project attribute' do
+ it 'returns the nodes if the attribute value equals the current project ID' do
+ link['data-project'] = project.id.to_s
+
+ expect(Ability.abilities).not_to receive(:allowed?)
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns the nodes if the user can read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the attribute value is empty' do
+ link['data-project'] = ''
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user can not read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+
+ describe '#nodes_user_can_reference' do
+ it 'returns the nodes' do
+ link = double(:link)
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+ end
+
+ describe '#referenced_by' do
+ context 'when references_relation is implemented' do
+ it 'returns a collection of objects' do
+ links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>").
+ children
+
+ expect(subject).to receive(:references_relation).and_return(User)
+ expect(subject.referenced_by(links)).to eq([user])
+ end
+ end
+
+ context 'when references_relation is not implemented' do
+ it 'raises NotImplementedError' do
+ links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children
+
+ expect { subject.referenced_by(links) }.
+ to raise_error(NotImplementedError)
+ end
+ end
+ end
+
+ describe '#references_relation' do
+ it 'raises NotImplementedError' do
+ expect { subject.references_relation }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#gather_attributes_per_project' do
+ it 'returns a Hash containing attribute values per project' do
+ link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>').
+ children[0]
+
+ hash = subject.gather_attributes_per_project([link], 'data-foo')
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[1].to_a).to eq(['2'])
+ end
+ end
+
+ describe '#grouped_objects_for_nodes' do
+ it 'returns a Hash grouping objects per ID' do
+ nodes = [double(:node)]
+
+ expect(subject).to receive(:unique_attribute_values).
+ with(nodes, 'data-user').
+ and_return([user.id])
+
+ hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
+
+ expect(hash).to eq({ user.id => user })
+ end
+
+ it 'returns an empty Hash when the list of nodes is empty' do
+ expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
+ end
+ end
+
+ describe '#unique_attribute_values' do
+ it 'returns an Array of unique values' do
+ link = double(:link)
+
+ expect(link).to receive(:has_attribute?).
+ with('data-foo').
+ twice.
+ and_return(true)
+
+ expect(link).to receive(:attr).
+ with('data-foo').
+ twice.
+ and_return('1')
+
+ nodes = [link, link]
+
+ expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1'])
+ end
+ end
+
+ describe '#process' do
+ it 'gathers the references for every node matching the reference type' do
+ dummy = Class.new(described_class) do
+ self.reference_type = :test
+ end
+
+ instance = dummy.new(project, user)
+ document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
+
+ expect(instance).to receive(:gather_references).
+ with([document.children[1]]).
+ and_return([user])
+
+ expect(instance.process([document])).to eq([user])
+ end
+ end
+
+ describe '#gather_references' do
+ let(:link) { double(:link) }
+
+ it 'does not process links a user can not reference' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([])
+
+ expect(subject).to receive(:referenced_by).with([])
+
+ subject.gather_references([link])
+ end
+
+ it 'does not process links a user can not see' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:nodes_visible_to_user).
+ with(user, [link]).
+ and_return([])
+
+ expect(subject).to receive(:referenced_by).with([])
+
+ subject.gather_references([link])
+ end
+
+ it 'returns the references if a user can reference and see a link' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:nodes_visible_to_user).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:referenced_by).with([link])
+
+ subject.gather_references([link])
+ end
+ end
+
+ describe '#can?' do
+ it 'delegates the permissions check to the Ability class' do
+ user = double(:user)
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, project)
+
+ subject.can?(user, :read_project, project)
+ end
+ end
+
+ describe '#find_projects_for_hash_keys' do
+ it 'returns a list of Projects' do
+ expect(subject.find_projects_for_hash_keys(project.id => project)).
+ to eq([project])
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
new file mode 100644
index 00000000000..0b76d29fce0
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::CommitParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link has a data-commit attribute' do
+ before do
+ link['data-commit'] = '123'
+ end
+
+ it 'returns an Array of commits' do
+ commit = double(:commit)
+
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject).to receive(:find_commits).
+ with(project, ['123']).
+ and_return([commit])
+
+ expect(subject.referenced_by([link])).to eq([commit])
+ end
+
+ it 'returns an empty Array when the commit could not be found' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject).to receive(:find_commits).
+ with(project, ['123']).
+ and_return([])
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+
+ it 'skips projects without valid repositories' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(false)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-commit attribute' do
+ it 'returns an empty Array' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#commit_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing commit IDs per project' do
+ link['data-commit'] = '123'
+
+ hash = subject.commit_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123'])
+ end
+
+ it 'does not add a project when the data-commit attribute is empty' do
+ hash = subject.commit_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+
+ describe '#find_commits' do
+ it 'returns an Array of commit objects' do
+ commit = double(:commit)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+ expect(project).to receive(:valid_repo?).and_return(true)
+
+ expect(subject.find_commits(project, %w{123})).to eq([commit])
+ end
+
+ it 'skips commit IDs for which no commit could be found' do
+ expect(project).to receive(:commit).with('123').and_return(nil)
+ expect(project).to receive(:valid_repo?).and_return(true)
+
+ expect(subject.find_commits(project, %w{123})).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
new file mode 100644
index 00000000000..ba982f38542
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link as a data-commit-range attribute' do
+ before do
+ link['data-commit-range'] = '123..456'
+ end
+
+ it 'returns an Array of commit ranges' do
+ range = double(:range)
+
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(range)
+
+ expect(subject.referenced_by([link])).to eq([range])
+ end
+
+ it 'returns an empty Array when the commit range could not be found' do
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(nil)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-commit-range attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#commit_range_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing range IDs per project' do
+ link['data-commit-range'] = '123..456'
+
+ hash = subject.commit_range_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123..456'])
+ end
+
+ it 'does not add a project when the data-commit-range attribute is empty' do
+ hash = subject.commit_range_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+
+ describe '#find_ranges' do
+ it 'returns an Array of range objects' do
+ range = double(:commit)
+
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(range)
+
+ expect(subject.find_ranges(project, ['123..456'])).to eq([range])
+ end
+
+ it 'skips ranges that could not be found' do
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(nil)
+
+ expect(subject.find_ranges(project, ['123..456'])).to eq([])
+ end
+ end
+
+ describe '#find_object' do
+ let(:range) { double(:range) }
+
+ before do
+ expect(CommitRange).to receive(:new).and_return(range)
+ end
+
+ context 'when the range has valid commits' do
+ it 'returns the commit range' do
+ expect(range).to receive(:valid_commits?).and_return(true)
+
+ expect(subject.find_object(project, '123..456')).to eq(range)
+ end
+ end
+
+ context 'when the range does not have any valid commits' do
+ it 'returns nil' do
+ expect(range).to receive(:valid_commits?).and_return(false)
+
+ expect(subject.find_object(project, '123..456')).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
new file mode 100644
index 00000000000..a6ef8394fe7
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link has a data-external-issue attribute' do
+ it 'returns an Array of ExternalIssue instances' do
+ link['data-external-issue'] = '123'
+
+ refs = subject.referenced_by([link])
+
+ expect(refs).to eq([ExternalIssue.new('123', project)])
+ end
+ end
+
+ context 'when the link does not have a data-external-issue attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#issue_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing range IDs per project' do
+ link['data-external-issue'] = '123'
+
+ hash = subject.issue_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123'])
+ end
+
+ it 'does not add a project when the data-external-issue attribute is empty' do
+ hash = subject.issue_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
new file mode 100644
index 00000000000..514c752546d
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::IssueParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before do
+ link['data-issue'] = issue.id.to_s
+ end
+
+ it 'returns the nodes when the user can read the issue' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_issue, issue).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the user can not read the issue' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_issue, issue).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-issue attribute' do
+ it 'returns an empty Array' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the project uses an external issue tracker' do
+ it 'returns all nodes' do
+ link = double(:link)
+
+ expect(project).to receive(:external_issue_tracker).and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+
+ describe '#referenced_by' do
+ context 'when the link has a data-issue attribute' do
+ context 'using an existing issue ID' do
+ before do
+ link['data-issue'] = issue.id.to_s
+ end
+
+ it 'returns an Array of issues' do
+ expect(subject.referenced_by([link])).to eq([issue])
+ end
+
+ it 'returns an empty Array when the list of nodes is empty' do
+ expect(subject.referenced_by([link])).to eq([issue])
+ expect(subject.referenced_by([])).to eq([])
+ end
+ end
+ end
+ end
+
+ describe '#issues_for_nodes' do
+ it 'returns a Hash containing the issues for a list of nodes' do
+ link['data-issue'] = issue.id.to_s
+ nodes = [link]
+
+ expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue })
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
new file mode 100644
index 00000000000..77fda47f0e7
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::LabelParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-label attribute' do
+ context 'using an existing label ID' do
+ it 'returns an Array of labels' do
+ link['data-label'] = label.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([label])
+ end
+ end
+
+ context 'using a non-existing label ID' do
+ it 'returns an empty Array' do
+ link['data-label'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
new file mode 100644
index 00000000000..cf89ad598ea
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ subject { described_class.new(merge_request.target_project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-merge-request attribute' do
+ context 'using an existing merge request ID' do
+ it 'returns an Array of merge requests' do
+ link['data-merge-request'] = merge_request.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([merge_request])
+ end
+ end
+
+ context 'using a non-existing merge request ID' do
+ it 'returns an empty Array' do
+ link['data-merge-request'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
new file mode 100644
index 00000000000..6aa45a22cc4
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MilestoneParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-milestone attribute' do
+ context 'using an existing milestone ID' do
+ it 'returns an Array of milestones' do
+ link['data-milestone'] = milestone.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([milestone])
+ end
+ end
+
+ context 'using a non-existing milestone ID' do
+ it 'returns an empty Array' do
+ link['data-milestone'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
new file mode 100644
index 00000000000..59127b7c5d1
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::SnippetParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:snippet) { create(:snippet, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-snippet attribute' do
+ context 'using an existing snippet ID' do
+ it 'returns an Array of snippets' do
+ link['data-snippet'] = snippet.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([snippet])
+ end
+ end
+
+ context 'using a non-existing snippet ID' do
+ it 'returns an empty Array' do
+ link['data-snippet'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
new file mode 100644
index 00000000000..9a82891297d
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -0,0 +1,189 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::UserParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public, group: group, creator: user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ end
+
+ it 'returns the users of the group' do
+ create(:group_member, group: group, user: user)
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+
+ it 'returns an empty Array when the group has no users' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'using a non-existing group ID' do
+ it 'returns an empty Array' do
+ link['data-group'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-user attribute' do
+ it 'returns an Array of users' do
+ link['data-user'] = user.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+ end
+
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID' do
+ let(:contributor) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ project.team << [contributor, :developer]
+ end
+
+ it 'returns the members of a project' do
+ link['data-project'] = project.id.to_s
+
+ # This uses an explicit sort to make sure this spec doesn't randomly
+ # fail when objects are returned in a different order.
+ refs = subject.referenced_by([link]).sort_by(&:id)
+
+ expect(refs).to eq([user, contributor])
+ end
+ end
+
+ context 'using a non-existing project ID' do
+ it 'returns an empty Array' do
+ link['data-project'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+
+ describe '#nodes_visible_to_use?' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = group.id.to_s
+ end
+
+ it 'returns the nodes if the user can read the group' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_group, group).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array if the user can not read the group' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_group, group).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-group attribute' do
+ context 'with a data-project attribute' do
+ it 'returns the nodes if the attribute value equals the current project ID' do
+ link['data-project'] = project.id.to_s
+
+ expect(Ability.abilities).not_to receive(:allowed?)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns the nodes if the user can read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array if the user can not read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'without a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#nodes_user_can_reference' do
+ context 'when the link has a data-author attribute' do
+ it 'returns the nodes when the user is a member of the project' do
+ other_project = create(:project)
+ other_project.team << [user, :developer]
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the project could not be found' do
+ link['data-project'] = ''
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user could not be found' do
+ other_project = create(:project)
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = ''
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user is not a team member' do
+ other_project = create(:project)
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-author attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+ end
+ end
+end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
index 3a2b568f4c7..898f1e84ab0 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -4,131 +4,185 @@ describe Ci::Ansi2html, lib: true do
subject { Ci::Ansi2html }
it "prints non-ansi as-is" do
- expect(subject.convert("Hello")).to eq('Hello')
+ expect(subject.convert("Hello")[:html]).to eq('Hello')
end
it "strips non-color-changing controll sequences" do
- expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world')
+ expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world')
end
it "prints simply red" do
- expect(subject.convert("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>')
+ expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('<span class="term-fg-red">Hello</span>')
end
it "prints simply red without trailing reset" do
- expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
+ expect(subject.convert("\e[31mHello")[:html]).to eq('<span class="term-fg-red">Hello</span>')
end
it "prints simply yellow" do
- expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
+ expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('<span class="term-fg-yellow">Hello</span>')
end
it "prints default on blue" do
- expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
+ expect(subject.convert("\e[39;44mHello")[:html]).to eq('<span class="term-bg-blue">Hello</span>')
end
it "prints red on blue" do
- expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
+ expect(subject.convert("\e[31;44mHello")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
end
it "resets colors after red on blue" do
- expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
+ expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
end
it "performs color change from red/blue to yellow/blue" do
- expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
+ expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
end
it "performs color change from red/blue to yellow/green" do
- expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
+ expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
end
it "performs color change from red/blue to reset to yellow/green" do
- expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
+ expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
end
it "ignores unsupported codes" do
- expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello')
+ expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello')
end
it "prints light red" do
- expect(subject.convert("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>')
+ expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red">Hello</span>')
end
it "prints default on light red" do
- expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
+ expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('<span class="term-bg-l-red">Hello</span>')
end
it "performs color change from red/blue to default/blue" do
- expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
end
it "performs color change from light red/blue to default/blue" do
- expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
end
it "prints bold text" do
- expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
+ expect(subject.convert("\e[1mHello")[:html]).to eq('<span class="term-bold">Hello</span>')
end
it "resets bold text" do
- expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
- expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
+ expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('<span class="term-bold">Hello</span> world')
+ expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('<span class="term-bold">Hello</span> world')
end
it "prints italic text" do
- expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
+ expect(subject.convert("\e[3mHello")[:html]).to eq('<span class="term-italic">Hello</span>')
end
it "resets italic text" do
- expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
+ expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('<span class="term-italic">Hello</span> world')
end
it "prints underlined text" do
- expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
+ expect(subject.convert("\e[4mHello")[:html]).to eq('<span class="term-underline">Hello</span>')
end
it "resets underlined text" do
- expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
+ expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('<span class="term-underline">Hello</span> world')
end
it "prints concealed text" do
- expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
+ expect(subject.convert("\e[8mHello")[:html]).to eq('<span class="term-conceal">Hello</span>')
end
it "resets concealed text" do
- expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
+ expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('<span class="term-conceal">Hello</span> world')
end
it "prints crossed-out text" do
- expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
+ expect(subject.convert("\e[9mHello")[:html]).to eq('<span class="term-cross">Hello</span>')
end
it "resets crossed-out text" do
- expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
+ expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('<span class="term-cross">Hello</span> world')
end
it "can print 256 xterm fg colors" do
- expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
+ expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('<span class="xterm-fg-16">Hello</span>')
end
it "can print 256 xterm fg colors on normal magenta background" do
- expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
+ expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
end
it "can print 256 xterm bg colors" do
- expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
+ expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('<span class="xterm-bg-240">Hello</span>')
end
it "can print 256 xterm bg colors on normal magenta foreground" do
- expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
+ expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
end
it "prints bold colored text vividly" do
- expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
end
it "prints bold light colored text correctly" do
- expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+
+ it "prints &lt;" do
+ expect(subject.convert("<")[:html]).to eq('&lt;')
+ end
+
+ describe "incremental update" do
+ shared_examples 'stateable converter' do
+ let(:pass1) { subject.convert(pre_text) }
+ let(:pass2) { subject.convert(pre_text + text, pass1[:state]) }
+
+ it "to returns html to append" do
+ expect(pass2[:append]).to be_truthy
+ expect(pass2[:html]).to eq(html)
+ expect(pass1[:text] + pass2[:text]).to eq(pre_text + text)
+ expect(pass1[:html] + pass2[:html]).to eq(pre_html + html)
+ end
+ end
+
+ context "with split word" do
+ let(:pre_text) { "\e[1mHello" }
+ let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
+ let(:text) { "\e[1mWorld" }
+ let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context "with split sequence" do
+ let(:pre_text) { "\e[1m" }
+ let(:pre_html) { "<span class=\"term-bold\"></span>" }
+ let(:text) { "Hello" }
+ let(:html) { "<span class=\"term-bold\">Hello</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context "with partial sequence" do
+ let(:pre_text) { "Hello\e" }
+ let(:pre_html) { "Hello" }
+ let(:text) { "[1m World" }
+ let(:html) { "<span class=\"term-bold\"> World</span>" }
+
+ it_behaves_like 'stateable converter'
+ end
+
+ context 'with new line' do
+ let(:pre_text) { "Hello\r" }
+ let(:pre_html) { "Hello\r" }
+ let(:text) { "\nWorld" }
+ let(:html) { "<br>World" }
+
+ it_behaves_like 'stateable converter'
+ end
end
end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 50a77308cde..97f2e97b062 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -1,16 +1,22 @@
require 'spec_helper'
describe Ci::Charts, lib: true do
-
context "build_times" do
before do
- @commit = FactoryGirl.create(:ci_commit)
- FactoryGirl.create(:ci_build, commit: @commit)
+ @pipeline = FactoryGirl.create(:ci_pipeline)
+ FactoryGirl.create(:ci_build, pipeline: @pipeline)
end
it 'should return build times in minutes' do
- chart = Ci::Charts::BuildTime.new(@commit.project)
+ chart = Ci::Charts::BuildTime.new(@pipeline.project)
expect(chart.build_times).to eq([2])
end
+
+ it 'should handle nil build times' do
+ create(:ci_pipeline, duration: nil, project: @pipeline.project)
+
+ chart = Ci::Charts::BuildTime.new(@pipeline.project)
+ expect(chart.build_times).to eq([2, 0])
+ 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 c7ab3185378..bad439bc489 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -26,7 +26,8 @@ module Ci
tag_list: [],
options: {},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
@@ -142,7 +143,6 @@ module Ci
end
it "returns build only for specified type" do
-
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
@@ -156,6 +156,35 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:only) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
end
describe :except do
@@ -283,16 +312,44 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
end
- end
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:except) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
+ end
end
-
+
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
-
+
subject { config_processor.builds_for_stage_and_ref("test", "master").first }
-
+
describe "before_script" do
context "in global context" do
let(:config) do
@@ -301,12 +358,12 @@ module Ci
test: { script: ["script"] }
}
end
-
+
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("global script\nscript")
end
end
-
+
context "overwritten in local context" do
let(:config) do
{
@@ -387,7 +444,8 @@ module Ci
services: ["mysql"]
},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
@@ -415,7 +473,8 @@ module Ci
services: ["postgresql"]
},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
end
@@ -443,12 +502,12 @@ module Ci
context 'when job variables are defined' do
context 'when syntax is correct' do
it 'returns job variables' do
- variables = {
+ variables = {
KEY1: 'value1',
SOME_KEY_2: 'value2'
}
- config = YAML.dump(
+ config = YAML.dump(
{ before_script: ['pwd'],
rspec: {
variables: variables,
@@ -462,19 +521,41 @@ module Ci
end
context 'when syntax is incorrect' do
- it 'raises error' do
- variables = [:KEY1, 'value1', :KEY2, 'value2']
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ context 'when variables defined but invalid' do
+ it 'raises error' do
+ variables = [:KEY1, 'value1', :KEY2, 'value2']
+
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: variables,
+ script: 'rspec' }
+ })
+
+ expect { GitlabCiYamlProcessor.new(config, path) }
+ .to raise_error(GitlabCiYamlProcessor::ValidationError,
+ /job: variables should be a map/)
+ end
+ end
- expect { GitlabCiYamlProcessor.new(config, path) }
- .to raise_error(GitlabCiYamlProcessor::ValidationError,
- /job: variables should be a map/)
+ context 'when variables key defined but value not specified' do
+ it 'returns empty array' do
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: nil,
+ script: 'rspec' }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ ##
+ # When variables config is empty, we assume this is a valid
+ # configuration, see issue #18775
+ #
+ expect(config_processor.job_variables(:rspec))
+ .to be_an_instance_of(Array).and be_empty
+ end
end
end
end
@@ -501,6 +582,7 @@ module Ci
})
config_processor = GitlabCiYamlProcessor.new(config, path)
+
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
@@ -508,7 +590,20 @@ module Ci
end
end
- describe "Caches" do
+ describe 'cache' do
+ context 'when cache definition has unknown keys' do
+ it 'raises relevant validation error' do
+ config = YAML.dump(
+ { cache: { untracked: true, invalid: 'key' },
+ rspec: { script: 'rspec' } })
+
+ expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
+ GitlabCiYamlProcessor::ValidationError,
+ 'cache config contains unknown keys: invalid'
+ )
+ end
+ end
+
it "returns cache when defined globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
@@ -572,7 +667,12 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ name: "custom_name",
+ expire_in: "7d"
+ },
script: "rspec"
}
})
@@ -594,13 +694,77 @@ module Ci
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
- untracked: true
+ untracked: true,
+ expire_in: "7d"
}
},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
+
+ %w[on_success on_failure always].each do |when_state|
+ it "returns artifacts for when #{when_state} defined" do
+ config = YAML.dump({
+ rspec: {
+ script: "rspec",
+ artifacts: { paths: ["logs/", "binaries/"], when: when_state }
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe '#environment' do
+ let(:config) do
+ {
+ deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
+ }
+ end
+
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+ let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
+
+ context 'when a production environment is specified' do
+ let(:environment) { 'production' }
+
+ it 'does return production' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment)
+ end
+ end
+
+ context 'when no environment is specified' do
+ let(:environment) { nil }
+
+ it 'does return nil environment' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to be_nil
+ end
+ end
+
+ context 'is not a string' do
+ let(:environment) { 1 }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
+
+ context 'is not a valid string' do
+ let(:environment) { 'production staging' }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
end
describe "Dependencies" do
@@ -619,19 +783,19 @@ module Ci
context 'no dependencies' do
let(:dependencies) { }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { ['build1', 'build2'] }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'undefined dependency' do
@@ -664,7 +828,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
end
@@ -709,7 +874,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
expect(subject.second).to eq({
except: nil,
@@ -721,7 +887,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
end
@@ -796,7 +963,7 @@ EOT
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
end
it "returns errors if job before_script parameter is not an array of strings" do
@@ -810,7 +977,7 @@ EOT
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
end
it "returns errors if job after_script parameter is not an array of strings" do
@@ -824,7 +991,7 @@ EOT
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a string")
end
it "returns errors if job name is blank" do
@@ -852,14 +1019,14 @@ EOT
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings")
end
it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings")
end
it "returns errors if job services parameter is not an array" do
@@ -926,31 +1093,31 @@ EOT
end
it "returns errors if stages is not an array" do
- config = YAML.dump({ types: "test", rspec: { script: "test" } })
+ config = YAML.dump({ stages: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if stages is not an array of strings" do
- config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } })
+ config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if job when is not on_success, on_failure or always" do
@@ -967,6 +1134,27 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
end
+ it "returns errors if job artifacts:when is not an a predefined value" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a valid duration" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ 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
@@ -985,21 +1173,21 @@ EOT
config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
end
it "returns errors if cache:paths is not an array of strings" do
config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config 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.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
end
it "returns errors if job cache:key is not an a string" do
@@ -1030,5 +1218,17 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
end
end
+
+ describe "Validate configuration templates" do
+ templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
+
+ templates.each do |file|
+ it "does not return errors for #{file}" do
+ file = File.read(file)
+
+ expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error
+ end
+ end
+ end
end
end
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
new file mode 100644
index 00000000000..4d8cb787dde
--- /dev/null
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Blob do
+ let(:digest) { 'sha256:0123456789012345' }
+ let(:config) do
+ {
+ 'digest' => digest,
+ 'mediaType' => 'binary',
+ 'size' => 1000
+ }
+ end
+
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+ let(:blob) { repository.blob(config) }
+
+ it { expect(blob).to respond_to(:repository) }
+ it { expect(blob).to delegate_method(:registry).to(:repository) }
+ it { expect(blob).to delegate_method(:client).to(:repository) }
+
+ context '#path' do
+ subject { blob.path }
+
+ it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') }
+ end
+
+ context '#digest' do
+ subject { blob.digest }
+
+ it { is_expected.to eq(digest) }
+ end
+
+ context '#type' do
+ subject { blob.type }
+
+ it { is_expected.to eq('binary') }
+ end
+
+ context '#revision' do
+ subject { blob.revision }
+
+ it { is_expected.to eq('0123456789012345') }
+ end
+
+ context '#short_revision' do
+ subject { blob.short_revision }
+
+ it { is_expected.to eq('012345678') }
+ end
+
+ context '#delete' do
+ before do
+ stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ to_return(status: 200)
+ end
+
+ subject { blob.delete }
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb
new file mode 100644
index 00000000000..4f3f8b24fc4
--- /dev/null
+++ b/spec/lib/container_registry/registry_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Registry do
+ let(:path) { nil }
+ let(:registry) { described_class.new('http://example.com', path: path) }
+
+ subject { registry }
+
+ it { is_expected.to respond_to(:client) }
+ it { is_expected.to respond_to(:uri) }
+ it { is_expected.to respond_to(:path) }
+
+ it { expect(subject.repository('test')).not_to be_nil }
+
+ context '#path' do
+ subject { registry.path }
+
+ context 'path from URL' do
+ it { is_expected.to eq('example.com') }
+ end
+
+ context 'custom path' do
+ let(:path) { 'registry.example.com' }
+
+ it { is_expected.to eq(path) }
+ end
+ end
+end
diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb
new file mode 100644
index 00000000000..c364e759108
--- /dev/null
+++ b/spec/lib/container_registry/repository_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Repository do
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+
+ it { expect(repository).to respond_to(:registry) }
+ it { expect(repository).to delegate_method(:client).to(:registry) }
+ it { expect(repository.tag('test')).not_to be_nil }
+
+ context '#path' do
+ subject { repository.path }
+
+ it { is_expected.to eq('example.com/group/test') }
+ end
+
+ context 'manifest processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/tags/list').
+ with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }).
+ to_return(
+ status: 200,
+ body: JSON.dump(tags: ['test']),
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ context '#manifest' do
+ subject { repository.manifest }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#valid?' do
+ subject { repository.valid? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context '#tags' do
+ subject { repository.tags }
+
+ it { is_expected.not_to be_empty }
+ end
+ end
+
+ context '#delete_tags' do
+ let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') }
+
+ before { expect(repository).to receive(:tags).twice.and_return([tag]) }
+
+ subject { repository.delete_tags }
+
+ context 'succeeds' do
+ before { expect(tag).to receive(:delete).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'any fails' do
+ before { expect(tag).to receive(:delete).and_return(false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
new file mode 100644
index 00000000000..c7324c2bf77
--- /dev/null
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Tag do
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+ let(:tag) { repository.tag('tag') }
+ let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } }
+
+ it { expect(tag).to respond_to(:repository) }
+ it { expect(tag).to delegate_method(:registry).to(:repository) }
+ it { expect(tag).to delegate_method(:client).to(:repository) }
+
+ context '#path' do
+ subject { tag.path }
+
+ it { is_expected.to eq('example.com/group/test:tag') }
+ end
+
+ context 'manifest processing' do
+ context 'schema v1' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest_1.json'),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v1+prettyjws' })
+ end
+
+ context '#layers' do
+ subject { tag.layers }
+
+ it { expect(subject.length).to eq(1) }
+ end
+
+ context '#total_size' do
+ subject { tag.total_size }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'config processing' do
+ context '#config' do
+ subject { tag.config }
+
+ it { is_expected.to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ context 'schema v2' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ end
+
+ context '#layers' do
+ subject { tag.layers }
+
+ it { expect(subject.length).to eq(1) }
+ end
+
+ context '#total_size' do
+ subject { tag.total_size }
+
+ it { is_expected.to eq(2319870) }
+ end
+
+ context 'config processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ context '#config' do
+ subject { tag.config }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
+
+ it { is_expected.not_to be_nil }
+ end
+ end
+ end
+ end
+
+ context 'manifest digest' do
+ before do
+ stub_request(:head, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
+ end
+
+ context '#digest' do
+ subject { tag.digest }
+
+ it { is_expected.to eq('sha256:digest') }
+ end
+
+ context '#delete' do
+ before do
+ stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest').
+ with(headers: headers).
+ to_return(status: 200)
+ end
+
+ subject { tag.delete }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index c2a7b20b84d..309a88151cf 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe DisableEmailInterceptor, lib: true do
before do
- ActionMailer::Base.register_interceptor(DisableEmailInterceptor)
+ Mail.register_interceptor(DisableEmailInterceptor)
end
it 'should not send emails' do
@@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do
# Removing interceptor from the list because unregister_interceptor is
# implemented in later version of mail gem
# See: https://github.com/mikel/mail/pull/705
- Mail.class_variable_set(:@@delivery_interceptors, [])
+ Mail.unregister_interceptor(DisableEmailInterceptor)
end
def deliver_mail
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
index 53f5d6c5c80..88a71528867 100644
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ b/spec/lib/gitlab/akismet_helper_spec.rb
@@ -6,8 +6,8 @@ describe Gitlab::AkismetHelper, type: :helper do
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'
+ allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
end
describe '#check_for_spam?' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 736bf787208..32ca8239845 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -3,13 +3,11 @@ require 'nokogiri'
module Gitlab
describe Asciidoc, lib: true do
-
let(:input) { '<b>ascii</b>' }
let(:context) { {} }
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
-
it "should convert the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
@@ -24,7 +22,6 @@ module Gitlab
end
context "with asciidoc_opts" do
-
let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
it "should merge the options with default ones" do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index aad291c03cd..7bec1367156 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -1,9 +1,47 @@
require 'spec_helper'
describe Gitlab::Auth, lib: true do
- let(:gl_auth) { Gitlab::Auth.new }
+ let(:gl_auth) { described_class }
- describe :find do
+ describe 'find_for_git_client' do
+ it 'recognizes CI' do
+ token = '123'
+ project = create(:empty_project)
+ project.update_attributes(runners_token: token, builds_enabled: true)
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
+ expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+ end
+
+ it 'recognizes master passwords' do
+ user = create(:user, password: 'password')
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+ end
+
+ it 'recognizes OAuth tokens' do
+ user = create(:user)
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+ end
+
+ it 'returns double nil for invalid credentials' do
+ login = 'foo'
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+ end
+ end
+
+ describe 'find_with_user_password' do
let!(:user) do
create(:user,
username: username,
@@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do
let(:password) { 'my-secret' }
it "should find user by valid login/password" do
- expect( gl_auth.find(username, password) ).to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).to eql user
end
it 'should find user by valid email/password with case-insensitive email' do
- expect(gl_auth.find(user.email.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
end
it 'should find user by valid username/password with case-insensitive username' do
- expect(gl_auth.find(username.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
end
it "should not find user with invalid password" do
password = 'wrong'
- expect( gl_auth.find(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
it "should not find user with invalid login" do
user = 'wrong'
- expect( gl_auth.find(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
context "with ldap enabled" do
@@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do
it "tries to autheticate with db before ldap" do
expect(Gitlab::LDAP::Authentication).not_to receive(:login)
- gl_auth.find(username, password)
+ gl_auth.find_with_user_password(username, password)
end
it "uses ldap as fallback to for authentication" do
expect(Gitlab::LDAP::Authentication).to receive(:login)
- gl_auth.find('ldap_user', 'password')
+ gl_auth.find_with_user_password('ldap_user', 'password')
end
end
end
diff --git a/spec/lib/gitlab/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
new file mode 100644
index 00000000000..00a110e31f8
--- /dev/null
+++ b/spec/lib/gitlab/award_emoji_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::AwardEmoji do
+ describe '.urls' do
+ after do
+ Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
+ end
+
+ subject { Gitlab::AwardEmoji.urls }
+
+ it { is_expected.to be_an_instance_of(Array) }
+ it { is_expected.not_to be_empty }
+
+ context 'every Hash in the Array' do
+ it 'has the correct keys and values' do
+ subject.each do |hash|
+ expect(hash[:name]).to be_an_instance_of(String)
+ expect(hash[:path]).to be_an_instance_of(String)
+ end
+ end
+ end
+
+ context 'handles relative root' do
+ it 'includes the full path' do
+ allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
+
+ subject.each do |hash|
+ expect(hash[:name]).to be_an_instance_of(String)
+ expect(hash[:path]).to start_with('/gitlab')
+ end
+ end
+ end
+ end
+
+ describe '.emoji_by_category' do
+ it "only contains known categories" do
+ undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
+ expect(undefined_categories).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
deleted file mode 100644
index cd26dca0998..00000000000
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ /dev/null
@@ -1,209 +0,0 @@
-require "spec_helper"
-
-describe Grack::Auth, lib: true do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- let(:app) { lambda { |env| [200, {}, "Success!"] } }
- let!(:auth) { Grack::Auth.new(app) }
- let(:env) do
- {
- 'rack.input' => '',
- 'REQUEST_METHOD' => 'GET',
- 'QUERY_STRING' => 'service=git-upload-pack'
- }
- end
- let(:status) { auth.call(env).first }
-
- describe "#call" do
- context "when the project doesn't exist" do
- before do
- env["PATH_INFO"] = "doesnt/exist.git"
- end
-
- context "when no authentication is provided" do
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when username and password are provided" do
- context "when authentication fails" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when authentication succeeds" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
- end
- end
-
- context "when the Wiki for a project exists" do
- before do
- @wiki = ProjectWiki.new(project)
- env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs"
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- it "responds with the right project" do
- response = auth.call(env)
- json_body = ActiveSupport::JSON.decode(response[2][0])
-
- expect(response.first).to eq(200)
- expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace)
- end
- end
-
- context "when the project exists" do
- before do
- env["PATH_INFO"] = project.path_with_namespace + ".git"
- end
-
- context "when the project is public" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
-
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- context "when no authentication is provided" do
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when username and password are provided" do
- context "when authentication fails" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
-
- context "when the user is IP banned" do
- before do
- expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
- end
-
- context "when authentication succeeds" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
- end
-
- context "when the user is blocked" do
- before do
- user.block
- project.team << [user, :master]
- end
-
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
-
- context "when the user isn't blocked" do
- before do
- expect(Rack::Attack::Allow2Ban).to receive(:reset)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
-
- context "when blank password attempts follow a valid login" do
- let(:options) { Gitlab.config.rack_attack.git_basic_auth }
- let(:maxretry) { options[:maxretry] - 1 }
- let(:ip) { '1.2.3.4' }
-
- before do
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
- Rack::Attack::Allow2Ban.reset(ip, options)
- end
-
- after do
- Rack::Attack::Allow2Ban.reset(ip, options)
- end
-
- def attempt_login(include_password)
- password = include_password ? user.password : ""
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
- Grack::Auth.new(app)
- auth.call(env).first
- end
-
- it "repeated attempts followed by successful attempt" do
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
-
- expect(attempt_login(true)).to eq(200)
- expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
-
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
- end
- end
- end
-
- context "when the user doesn't have access to the project" do
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
- end
- end
-
- context "when a gitlab ci token is provided" do
- let(:token) { "123" }
- let(:project) { FactoryGirl.create :empty_project }
-
- before do
- project.update_attributes(runners_token: token, builds_enabled: true)
-
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index fd869f48b5c..6e5ba211382 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -13,9 +13,37 @@ describe Gitlab::Shell, lib: true do
it { is_expected.to respond_to :add_repository }
it { is_expected.to respond_to :remove_repository }
it { is_expected.to respond_to :fork_repository }
+ it { is_expected.to respond_to :gc }
+ it { is_expected.to respond_to :add_namespace }
+ it { is_expected.to respond_to :rm_namespace }
+ it { is_expected.to respond_to :mv_namespace }
+ it { is_expected.to respond_to :exists? }
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
+ describe 'generate_and_link_secret_token' do
+ let(:secret_file) { 'tmp/tests/.secret_shell_test' }
+ let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
+
+ before do
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
+ allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
+ FileUtils.mkdir('tmp/tests/shell-secret-test')
+ gitlab_shell.generate_and_link_secret_token
+ end
+
+ after do
+ FileUtils.rm_rf('tmp/tests/shell-secret-test')
+ FileUtils.rm_rf(secret_file)
+ end
+
+ it 'creates and links the secret token file' do
+ expect(File.exist?(secret_file)).to be(true)
+ expect(File.symlink?(link_file)).to be(true)
+ expect(File.readlink(link_file)).to eq(secret_file)
+ end
+ end
+
describe Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do
it 'normalizes space characters in the key' do
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
index b6f7a2e7ec4..2034445a197 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do
end
context 'build exists' do
- let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) }
- let!(:build) { create(:ci_build, commit: ci_commit) }
-
+ let!(:build) { create_build(project, sha, branch) }
context 'build success' do
before { build.success! }
@@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do
end
end
+ context 'when outdated pipeline for given ref exists' do
+ before do
+ build = create_build(project, sha, branch)
+ build.success!
+
+ old_build = create_build(project, '11eeffdd', branch)
+ old_build.drop!
+ end
+
+ it 'does not take outdated pipeline into account' do
+ expect(badge.to_s).to eq 'build-success'
+ end
+ end
+
+ def create_build(project, sha, branch)
+ pipeline = create(:ci_pipeline, project: project,
+ sha: sha,
+ ref: branch)
+
+ create(:ci_build, pipeline: pipeline)
+ end
+
def status_node(data, status)
xml = Nokogiri::XML.parse(data)
xml.at(%Q{text:contains("#{status}")})
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
index af839f42421..760d66a1488 100644
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe Gitlab::BitbucketImport::Client, lib: true do
+ include ImportSpecHelper
+
let(:token) { '123456' }
let(:secret) { 'secret' }
let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ stub_omniauth_provider('bitbucket')
end
it 'all OAuth client options are symbols' do
@@ -59,7 +61,7 @@ describe Gitlab::BitbucketImport::Client, lib: true do
bitbucket_access_token_secret: "test" } })
project.import_url = "ssh://git@bitbucket.org/test/test.git"
- expect { described_class.from_project(project) }.to_not raise_error
+ expect { described_class.from_project(project) }.not_to raise_error
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 1a833f255a5..aa00f32becb 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe Gitlab::BitbucketImport::Importer, lib: true do
+ include ImportSpecHelper
+
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ stub_omniauth_provider('bitbucket')
end
let(:statuses) do
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index acca0b08bab..abc93e1b44a 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -10,8 +10,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
'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/'=> {},
+ 'path/second_dir/dir_3/file_3' => { size: 10 },
+ 'another_directory/' => {},
'another_file' => {},
'/file/with/absolute_path' => {} }
end
@@ -122,13 +122,12 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'empty path', path: '' do
subject { |example| path(example) }
- it { is_expected.to_not have_parent }
+ it { is_expected.not_to 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
diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb
new file mode 100644
index 00000000000..2d44b1f60f1
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/loader_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Loader do
+ let(:loader) { described_class.new(yml) }
+
+ context 'when yaml syntax is correct' do
+ let(:yml) { 'image: ruby:2.2' }
+
+ describe '#valid?' do
+ it 'returns true' do
+ expect(loader.valid?).to be true
+ end
+ end
+
+ describe '#load!' do
+ it 'returns a valid hash' do
+ expect(loader.load!).to eq(image: 'ruby:2.2')
+ end
+ end
+ end
+
+ context 'when yaml syntax is incorrect' do
+ let(:yml) { '// incorrect' }
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader.valid?).to be false
+ end
+ end
+
+ describe '#load!' do
+ it 'raises error' do
+ expect { loader.load! }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ 'Invalid configuration format'
+ )
+ end
+ end
+ end
+
+ context 'when yaml config is empty' do
+ let(:yml) { '' }
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader.valid?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/boolean_spec.rb b/spec/lib/gitlab/ci/config/node/boolean_spec.rb
new file mode 100644
index 00000000000..deafa8bf8a7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/boolean_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Boolean do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ let(:config) { false }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq false
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'boolean config should be a boolean value'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb
new file mode 100644
index 00000000000..50f619ce26e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Cache do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ before { entry.process! }
+
+ context 'when entry config value is correct' do
+ let(:config) do
+ { key: 'some key',
+ untracked: true,
+ paths: ['some/path/'] }
+ end
+
+ describe '#value' do
+ it 'returns hash value' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ describe '#errors' do
+ context 'when is not a hash' do
+ let(:config) { 'ls' }
+
+ it 'reports errors with config value' do
+ expect(entry.errors)
+ .to include 'cache config should be a hash'
+ end
+ end
+
+ context 'when descendants are invalid' do
+ let(:config) { { key: 1 } }
+
+ it 'reports error with descendants' do
+ expect(entry.errors)
+ .to include 'key config should be a string or symbol'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { invalid: true } }
+
+ it 'reports error with descendants' do
+ expect(entry.errors)
+ .to include 'cache config contains unknown keys: invalid'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
new file mode 100644
index 00000000000..c468ecf957b
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Configurable do
+ let(:node) { Class.new }
+
+ before do
+ node.include(described_class)
+ end
+
+ describe 'validations' do
+ let(:validator) { node.validator.new(instance) }
+
+ before do
+ node.class_eval do
+ attr_reader :config
+
+ def initialize(config)
+ @config = config
+ end
+ end
+
+ validator.validate
+ end
+
+ context 'when node validator is invalid' do
+ let(:instance) { node.new('ls') }
+
+ it 'returns invalid validator' do
+ expect(validator).to be_invalid
+ end
+ end
+
+ context 'when node instance is valid' do
+ let(:instance) { node.new(key: 'value') }
+
+ it 'returns valid validator' do
+ expect(validator).to be_valid
+ end
+ end
+ end
+
+ describe 'configured nodes' do
+ before do
+ node.class_eval do
+ node :object, Object, description: 'test object'
+ end
+ end
+
+ describe '.nodes' do
+ it 'has valid nodes' do
+ expect(node.nodes).to include :object
+ end
+
+ it 'creates a node factory' do
+ expect(node.nodes[:object])
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
+ end
+
+ it 'returns a duplicated factory object' do
+ first_factory = node.nodes[:object]
+ second_factory = node.nodes[:object]
+
+ expect(first_factory).not_to be_equal(second_factory)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
new file mode 100644
index 00000000000..91ddef7bfbf
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Factory do
+ describe '#create!' do
+ let(:factory) { described_class.new(entry_class) }
+ let(:entry_class) { Gitlab::Ci::Config::Node::Script }
+
+ context 'when setting up a value' do
+ it 'creates entry with valid value' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .create!
+
+ expect(entry.value).to eq ['ls', 'pwd']
+ end
+
+ context 'when setting description' do
+ it 'creates entry with description' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .with(description: 'test description')
+ .create!
+
+ expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.description).to eq 'test description'
+ end
+ end
+
+ context 'when setting key' do
+ it 'creates entry with custom key' do
+ entry = factory
+ .with(value: ['ls', 'pwd'], key: 'test key')
+ .create!
+
+ expect(entry.key).to eq 'test key'
+ end
+ end
+
+ context 'when setting a parent' do
+ let(:parent) { Object.new }
+
+ it 'creates entry with valid parent' do
+ entry = factory
+ .with(value: 'ls', parent: parent)
+ .create!
+
+ expect(entry.parent).to eq parent
+ end
+ end
+ end
+
+ context 'when not setting up a value' do
+ it 'raises error' do
+ expect { factory.create! }.to raise_error(
+ Gitlab::Ci::Config::Node::Factory::InvalidFactory
+ )
+ end
+ end
+
+ context 'when creating entry with nil value' do
+ it 'creates an undefined entry' do
+ entry = factory
+ .with(value: nil)
+ .create!
+
+ expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
new file mode 100644
index 00000000000..c87c9e97bc8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Global do
+ let(:global) { described_class.new(hash) }
+
+ describe '.nodes' do
+ it 'can contain global config keys' do
+ expect(described_class.nodes).to include :before_script
+ end
+
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a Hash
+ end
+ end
+
+ context 'when hash is valid' do
+ context 'when all entries defined' do
+ let(:hash) do
+ { before_script: ['ls', 'pwd'],
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ variables: { VAR: 'value' },
+ after_script: ['make clean'],
+ stages: ['build', 'pages'],
+ cache: { key: 'k', untracked: true, paths: ['public/'] } }
+ end
+
+ describe '#process!' do
+ before { global.process! }
+
+ it 'creates nodes hash' do
+ expect(global.nodes).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.nodes.count).to eq 8
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.nodes.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Script
+ expect(global.nodes.second)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Image
+ end
+
+ it 'sets correct description for nodes' do
+ expect(global.nodes.first.description)
+ .to eq 'Script that will be executed before each job.'
+ expect(global.nodes.second.description)
+ .to eq 'Docker image that will be used to execute jobs.'
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+
+ context 'when not processed' do
+ describe '#before_script' do
+ it 'returns nil' do
+ expect(global.before_script).to be nil
+ end
+ end
+ end
+
+ context 'when processed' do
+ before { global.process! }
+
+ describe '#before_script' do
+ it 'returns correct script' do
+ expect(global.before_script).to eq ['ls', 'pwd']
+ end
+ end
+
+ describe '#image' do
+ it 'returns valid image' do
+ expect(global.image).to eq 'ruby:2.2'
+ end
+ end
+
+ describe '#services' do
+ it 'returns array of services' do
+ expect(global.services).to eq ['postgres:9.1', 'mysql:5.5']
+ end
+ end
+
+ describe '#after_script' do
+ it 'returns after script' do
+ expect(global.after_script).to eq ['make clean']
+ end
+ end
+
+ describe '#variables' do
+ it 'returns variables' do
+ expect(global.variables).to eq(VAR: 'value')
+ end
+ end
+
+ describe '#stages' do
+ context 'when stages key defined' do
+ it 'returns array of stages' do
+ expect(global.stages).to eq %w[build pages]
+ end
+ end
+
+ context 'when deprecated types key defined' do
+ let(:hash) { { types: ['test', 'deploy'] } }
+
+ it 'returns array of types as stages' do
+ expect(global.stages).to eq %w[test deploy]
+ end
+ end
+ end
+
+ describe '#cache' do
+ it 'returns cache configuration' do
+ expect(global.cache)
+ .to eq(key: 'k', untracked: true, paths: ['public/'])
+ end
+ end
+ end
+ end
+
+ context 'when most of entires not defined' do
+ let(:hash) { { cache: { key: 'a' }, rspec: {} } }
+ before { global.process! }
+
+ describe '#nodes' do
+ it 'instantizes all nodes' do
+ expect(global.nodes.count).to eq 8
+ end
+
+ it 'contains undefined nodes' do
+ expect(global.nodes.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ end
+ end
+
+ describe '#variables' do
+ it 'returns default value for variables' do
+ expect(global.variables).to eq({})
+ end
+ end
+
+ describe '#stages' do
+ it 'returns an array of default stages' do
+ expect(global.stages).to eq %w[build test deploy]
+ end
+ end
+
+ describe '#cache' do
+ it 'returns correct cache definition' do
+ expect(global.cache).to eq(key: 'a')
+ end
+ end
+ end
+
+ ##
+ # When nodes are specified but not defined, we assume that
+ # configuration is valid, and we asume that entry is simply undefined,
+ # despite the fact, that key is present. See issue #18775 for more
+ # details.
+ #
+ context 'when entires specified but not defined' do
+ let(:hash) { { variables: nil } }
+ before { global.process! }
+
+ describe '#variables' do
+ it 'undefined entry returns a default value' do
+ expect(global.variables).to eq({})
+ end
+ end
+ end
+ end
+
+ context 'when hash is not valid' do
+ before { global.process! }
+
+ let(:hash) do
+ { before_script: 'ls' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'reports errors from child nodes' do
+ expect(global.errors)
+ .to include 'before_script config should be an array of strings'
+ end
+ end
+
+ describe '#before_script' do
+ it 'raises error' do
+ expect { global.before_script }.to raise_error(
+ Gitlab::Ci::Config::Node::Entry::InvalidError
+ )
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'returns error about invalid type' do
+ expect(global.errors.first).to match /should be a hash/
+ end
+ end
+ end
+
+ describe '#defined?' do
+ it 'is concrete entry that is defined' do
+ expect(global.defined?).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/image_spec.rb b/spec/lib/gitlab/ci/config/node/image_spec.rb
new file mode 100644
index 00000000000..d11bb39f328
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/image_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Image do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validation' do
+ context 'when entry config value is correct' do
+ let(:config) { 'ruby:2.2' }
+
+ describe '#value' do
+ it 'returns image string' do
+ expect(entry.value).to eq 'ruby:2.2'
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { ['ruby:2.2'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'image config should be a string'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/key_spec.rb b/spec/lib/gitlab/ci/config/node/key_spec.rb
new file mode 100644
index 00000000000..8cda43173fe
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/key_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Key do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { 'test' }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq 'test'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { [ 'incorrect' ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'key config should be a string or symbol'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/paths_spec.rb b/spec/lib/gitlab/ci/config/node/paths_spec.rb
new file mode 100644
index 00000000000..6fd744b3975
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/paths_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Paths do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ let(:config) { ['some/file', 'some/path/'] }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [ 1 ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'paths config should be an array of strings'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
new file mode 100644
index 00000000000..ee7395362a9
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Script do
+ let(:entry) { described_class.new(config) }
+
+ describe '#process!' do
+ before { entry.process! }
+
+ context 'when entry config value is correct' do
+ let(:config) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'script config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/services_spec.rb b/spec/lib/gitlab/ci/config/node/services_spec.rb
new file mode 100644
index 00000000000..be0fe46befd
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/services_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Services do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { ['postgres:9.1', 'mysql:5.5'] }
+
+ describe '#value' do
+ it 'returns array of services as is' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'services config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/stages_spec.rb b/spec/lib/gitlab/ci/config/node/stages_spec.rb
new file mode 100644
index 00000000000..1a3818d8997
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/stages_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Stages do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { [:stage1, :stage2] }
+
+ describe '#value' do
+ it 'returns array of stages' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { { test: true } }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'stages config should be an array of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default stages' do
+ expect(described_class.default).to eq %w[build test deploy]
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
new file mode 100644
index 00000000000..0c6608d906d
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Undefined do
+ let(:undefined) { described_class.new(entry) }
+ let(:entry) { Class.new }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(undefined).to be_leaf
+ end
+ end
+
+ describe '#valid?' do
+ it 'is always valid' do
+ expect(undefined).to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'is does not contain errors' do
+ expect(undefined.errors).to be_empty
+ end
+ end
+
+ describe '#value' do
+ before do
+ allow(entry).to receive(:default).and_return('some value')
+ end
+
+ it 'returns default value for entry' do
+ expect(undefined.value).to eq 'some value'
+ end
+ end
+
+ describe '#undefined?' do
+ it 'is not a defined entry' do
+ expect(undefined.defined?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
new file mode 100644
index 00000000000..10cd01afcd1
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Validatable do
+ let(:node) { Class.new }
+
+ before do
+ node.include(described_class)
+ end
+
+ describe '.validator' do
+ before do
+ node.class_eval do
+ attr_accessor :test_attribute
+
+ validations do
+ validates :test_attribute, presence: true
+ end
+ end
+ end
+
+ it 'returns validator' do
+ expect(node.validator.superclass)
+ .to be Gitlab::Ci::Config::Node::Validator
+ end
+
+ context 'when validating node instance' do
+ let(:node_instance) { node.new }
+
+ context 'when attribute is valid' do
+ before do
+ node_instance.test_attribute = 'valid'
+ end
+
+ it 'instance of validator is valid' do
+ expect(node.validator.new(node_instance)).to be_valid
+ end
+ end
+
+ context 'when attribute is not valid' do
+ before do
+ node_instance.test_attribute = nil
+ end
+
+ it 'instance of validator is invalid' do
+ expect(node.validator.new(node_instance)).to be_invalid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/node/validator_spec.rb
new file mode 100644
index 00000000000..090fd63b844
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/validator_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Validator do
+ let(:validator) { Class.new(described_class) }
+ let(:validator_instance) { validator.new(node) }
+ let(:node) { spy('node') }
+
+ before do
+ allow(node).to receive(:key).and_return('node')
+ allow(node).to receive(:ancestors).and_return([])
+ end
+
+ describe 'delegated validator' do
+ before do
+ validator.class_eval do
+ validates :test_attribute, presence: true
+ end
+ end
+
+ context 'when node is valid' do
+ before do
+ allow(node).to receive(:test_attribute).and_return('valid value')
+ end
+
+ it 'validates attribute in node' do
+ expect(node).to receive(:test_attribute)
+ expect(validator_instance).to be_valid
+ end
+
+ it 'returns no errors' do
+ validator_instance.validate
+
+ expect(validator_instance.messages).to be_empty
+ end
+ end
+
+ context 'when node is invalid' do
+ before do
+ allow(node).to receive(:test_attribute).and_return(nil)
+ end
+
+ it 'validates attribute in node' do
+ expect(node).to receive(:test_attribute)
+ expect(validator_instance).to be_invalid
+ end
+
+ it 'returns errors' do
+ validator_instance.validate
+
+ expect(validator_instance.messages)
+ .to include "node test attribute can't be blank"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/variables_spec.rb b/spec/lib/gitlab/ci/config/node/variables_spec.rb
new file mode 100644
index 00000000000..4b6d971ec71
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/variables_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Variables do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ describe '#value' do
+ it 'returns hash with key value strings' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { [ :VAR, 'test' ] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /should be a hash of key value pairs/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
new file mode 100644
index 00000000000..bc5a5e43103
--- /dev/null
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config do
+ let(:config) do
+ described_class.new(yml)
+ end
+
+ context 'when config is valid' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ rspec:
+ script:
+ - gem install rspec
+ - rspec
+ EOS
+ end
+
+ describe '#to_hash' do
+ it 'returns hash created from string' do
+ hash = {
+ image: 'ruby:2.2',
+ rspec: {
+ script: ['gem install rspec',
+ 'rspec']
+ }
+ }
+
+ expect(config.to_hash).to eq hash
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when config is invalid' do
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ /Invalid configuration format/
+ )
+ end
+ end
+ end
+
+ context 'when config logic is incorrect' do
+ let(:yml) { 'before_script: "ls"' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'has errors' do
+ expect(config.errors).not_to be_empty
+ end
+ end
+
+ describe '#errors' do
+ it 'returns an array of strings' do
+ expect(config.errors).to all(be_an_instance_of(String))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
new file mode 100644
index 00000000000..004341ffd02
--- /dev/null
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::CurrentSettings do
+ describe '#current_application_settings' do
+ it 'attempts to use cached values first' do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+
+ it 'does not attempt to connect to DB or Redis' do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to eq fake_application_settings
+ end
+
+ it 'falls back to DB if Redis returns an empty value' do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ expect(ApplicationSetting).to receive(:last).and_call_original
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+
+ it 'falls back to DB if Redis fails' do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
+ expect(ApplicationSetting).to receive(:last).and_call_original
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
new file mode 100644
index 00000000000..9096ad101b0
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe Gitlab::Database::MigrationHelpers, lib: true do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(
+ Gitlab::Database::MigrationHelpers
+ )
+ end
+
+ before { allow(model).to receive(:puts) }
+
+ describe '#add_concurrent_index' do
+ context 'outside a transaction' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using PostgreSQL' do
+ before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) }
+
+ it 'creates the index concurrently' do
+ expect(model).to receive(:add_index).
+ with(:users, :foo, algorithm: :concurrently)
+
+ model.add_concurrent_index(:users, :foo)
+ end
+
+ it 'creates unique index concurrently' do
+ expect(model).to receive(:add_index).
+ with(:users, :foo, { algorithm: :concurrently, unique: true })
+
+ model.add_concurrent_index(:users, :foo, unique: true)
+ end
+ end
+
+ context 'using MySQL' do
+ it 'creates a regular index' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:add_index).
+ with(:users, :foo, {})
+
+ model.add_concurrent_index(:users, :foo)
+ end
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.add_concurrent_index(:users, :foo) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#update_column_in_batches' do
+ before do
+ create_list(:empty_project, 5)
+ end
+
+ it 'updates all the rows in a table' do
+ model.update_column_in_batches(:projects, :import_error, 'foo')
+
+ expect(Project.where(import_error: 'foo').count).to eq(5)
+ end
+
+ it 'updates boolean values correctly' do
+ model.update_column_in_batches(:projects, :archived, true)
+
+ expect(Project.where(archived: true).count).to eq(5)
+ end
+
+ context 'when a block is supplied' do
+ it 'yields an Arel table and query object to the supplied block' do
+ first_id = Project.first.id
+
+ model.update_column_in_batches(:projects, :archived, true) do |t, query|
+ query.where(t[:id].eq(first_id))
+ end
+
+ expect(Project.where(archived: true).count).to eq(1)
+ end
+ end
+ end
+
+ describe '#add_column_with_default' do
+ context 'outside of a transaction' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
+
+ expect(model).to receive(:transaction).and_yield
+
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil)
+
+ expect(model).to receive(:change_column_default).
+ with(:projects, :foo, 10)
+ end
+
+ it 'adds the column while allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
+
+ expect(model).not_to receive(:change_column_null)
+
+ model.add_column_with_default(:projects, :foo, :integer,
+ default: 10,
+ allow_null: true)
+ end
+
+ it 'adds the column while not allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
+
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false)
+
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end
+
+ it 'removes the added column whenever updating the rows fails' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+
+ it 'removes the added column whenever changing a column NULL constraint fails' do
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index d0a447753b7..3031559c613 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -39,6 +39,22 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_last_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'}
+ it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'}
+ it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'}
+ end
+ end
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index a0cbef6e6a4..1cb513d5229 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, [commit.parent, commit]) }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
describe :diff_lines do
let(:diff_lines) { diff_file.diff_lines }
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index d19bf4ac84b..fb5d50a5c68 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -6,11 +6,11 @@ describe Gitlab::Diff::Highlight, 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, [commit.parent, commit]) }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
describe '#highlight' do
context "with a diff file" do
- let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight }
+ let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight }
it 'should return Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
@@ -41,7 +41,7 @@ describe Gitlab::Diff::Highlight, lib: true do
end
context "with diff lines" do
- let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight }
+ let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight }
it 'should return Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index ea5c31011f0..198ff977f24 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -2,7 +2,6 @@ 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 }
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
new file mode 100644
index 00000000000..4e50e03bb7e
--- /dev/null
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::LineMapper, 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_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
+ subject { described_class.new(diff_file) }
+
+ describe '#old_to_new' do
+ context "with a diff file" do
+ let(:mapping) do
+ {
+ 1 => 1,
+ 2 => 2,
+ 3 => 3,
+ 4 => 4,
+ 5 => 5,
+ 6 => 6,
+ 7 => 7,
+ 8 => 8,
+ 9 => nil,
+ # nil => 9,
+ 10 => 10,
+ 11 => 11,
+ 12 => 12,
+ 13 => nil,
+ 14 => nil,
+ # nil => 15,
+ # nil => 16,
+ # nil => 17,
+ # nil => 18,
+ # nil => 19,
+ # nil => 20,
+ 15 => 21,
+ 16 => 22,
+ 17 => 23,
+ 18 => 24,
+ 19 => 25,
+ 20 => 26,
+ 21 => 27,
+ # nil => 28,
+ 22 => 29,
+ 23 => 30,
+ 24 => 31,
+ 25 => 32,
+ 26 => 33,
+ 27 => 34,
+ 28 => 35,
+ 29 => 36,
+ 30 => 37
+ }
+ end
+
+ it 'returns the new line number for the old line number' do
+ mapping.each do |old_line, new_line|
+ expect(subject.old_to_new(old_line)).to eq(new_line)
+ end
+ end
+ end
+
+ context "without a diff file" do
+ let(:diff_file) { nil }
+
+ it "returns the same line number" do
+ expect(subject.old_to_new(100)).to eq(100)
+ end
+ end
+ end
+
+ describe '#new_to_old' do
+ context "with a diff file" do
+ let(:mapping) do
+ {
+ 1 => 1,
+ 2 => 2,
+ 3 => 3,
+ 4 => 4,
+ 5 => 5,
+ 6 => 6,
+ 7 => 7,
+ 8 => 8,
+ # nil => 9,
+ 9 => nil,
+ 10 => 10,
+ 11 => 11,
+ 12 => 12,
+ # nil => 13,
+ # nil => 14,
+ 13 => nil,
+ 14 => nil,
+ 15 => nil,
+ 16 => nil,
+ 17 => nil,
+ 18 => nil,
+ 19 => nil,
+ 20 => nil,
+ 21 => 15,
+ 22 => 16,
+ 23 => 17,
+ 24 => 18,
+ 25 => 19,
+ 26 => 20,
+ 27 => 21,
+ 28 => nil,
+ 29 => 22,
+ 30 => 23,
+ 31 => 24,
+ 32 => 25,
+ 33 => 26,
+ 34 => 27,
+ 35 => 28,
+ 36 => 29,
+ 37 => 30
+ }
+ end
+
+ it 'returns the old line number for the new line number' do
+ mapping.each do |new_line, old_line|
+ expect(subject.new_to_old(new_line)).to eq(old_line)
+ end
+ end
+ end
+
+ context "without a diff file" do
+ let(:diff_file) { nil }
+
+ it "returns the same line number" do
+ expect(subject.new_to_old(100)).to eq(100)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index 1c5bbc47120..5f76b70c6f5 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -8,8 +8,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
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) }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
subject { described_class.new(diff_file) }
let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") }
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
new file mode 100644
index 00000000000..cf28628cb96
--- /dev/null
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -0,0 +1,341 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Position, lib: true do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+
+ describe "position for an added file" do
+ let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") }
+
+ subject do
+ described_class.new(
+ old_path: "files/images/wm.svg",
+ new_path: "files/images/wm.svg",
+ old_line: nil,
+ new_line: 5,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+ <desc>Created with Sketch.</desc>")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "position for a changed file" do
+ let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
+
+ describe "position for an added line" do
+ subject do
+ described_class.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+ vars = {")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "position for an unchanged line" do
+ subject do
+ described_class.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.unchanged?).to be true
+ expect(diff_line.old_line).to eq(subject.old_line)
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq(" unless File.directory?(path)")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "position for a removed line" do
+ subject do
+ described_class.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 14,
+ new_line: nil,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.removed?).to be true
+ expect(diff_line.old_line).to eq(subject.old_line)
+ expect(diff_line.text).to eq("- options = { chdir: path }")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+ end
+
+ describe "position for a renamed file" do
+ let(:commit) { project.commit("6907208d755b60ebeacb2e9dfea74c92c3449a1f") }
+
+ describe "position for an added line" do
+ subject do
+ described_class.new(
+ old_path: "files/js/commit.js.coffee",
+ new_path: "files/js/commit.coffee",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+ new CommitFile(@)")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "position for an unchanged line" do
+ subject do
+ described_class.new(
+ old_path: "files/js/commit.js.coffee",
+ new_path: "files/js/commit.coffee",
+ old_line: 3,
+ new_line: 3,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.unchanged?).to be true
+ expect(diff_line.old_line).to eq(subject.old_line)
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq(" $('.files .diff-file').each ->")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "position for a removed line" do
+ subject do
+ described_class.new(
+ old_path: "files/js/commit.js.coffee",
+ new_path: "files/js/commit.coffee",
+ old_line: 4,
+ new_line: nil,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.removed?).to be true
+ expect(diff_line.old_line).to eq(subject.old_line)
+ expect(diff_line.text).to eq("- new CommitFile(this)")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+ end
+
+ describe "position for a deleted file" do
+ let(:commit) { project.commit("8634272bfad4cf321067c3e94d64d5a253f8321d") }
+
+ subject do
+ described_class.new(
+ old_path: "LICENSE",
+ new_path: "LICENSE",
+ old_line: 3,
+ new_line: nil,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.deleted_file).to be true
+ expect(diff_file.old_path).to eq(subject.old_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.removed?).to be true
+ expect(diff_line.old_line).to eq(subject.old_line)
+ expect(diff_line.text).to eq("-Copyright (c) 2014 gitlabhq")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
new file mode 100644
index 00000000000..08312e60f4a
--- /dev/null
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -0,0 +1,1758 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::PositionTracer, lib: true do
+ # Douwe's diary New York City, 2016-06-28
+ # --------------------------------------------------------------------------
+ #
+ # Dear diary,
+ #
+ # Ideally, we would have a test for every single diff scenario that can
+ # occur and that the PositionTracer should correctly trace a position
+ # through, across the following variables:
+ #
+ # - Old diff file type: created, changed, renamed, deleted, unchanged (5)
+ # - Old diff line type: added, removed, unchanged (3)
+ # - New diff file type: created, changed, renamed, deleted, unchanged (5)
+ # - New diff line type: added, removed, unchanged (3)
+ # - Old-to-new diff line change: kept, moved, undone (3)
+ #
+ # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios,
+ # and 675 different tests to cover them all. In reality, it would be fewer,
+ # since one cannot have a removed line in a created file diff, for example,
+ # but for the sake of this diary entry, let's be pessimistic.
+ #
+ # Writing these tests is a manual and time consuming process, as every test
+ # requires the manual construction or finding of a combination of diffs that
+ # create the exact diff scenario we are looking for, and can take between
+ # 1 and 10 minutes, depending on the farfetchedness of the scenario and
+ # complexity of creating it.
+ #
+ # This means that writing tests to cover all of these scenarios would end up
+ # taking between 11 and 112 hours in total, which I do not believe is the
+ # best use of my time.
+ #
+ # A better course of action would be to think of scenarios that are likely
+ # to occur, but also potentially tricky to trace correctly, and only cover
+ # those, with a few more obvious scenarios thrown in to cover our bases.
+ #
+ # Unfortunately, I only came to the above realization once I was about
+ # 1/5th of the way through the process of writing ALL THE SPECS, having
+ # already wasted about 3 hours trying to be thorough.
+ #
+ # I did find 2 bugs while writing those though, so that's good.
+ #
+ # In any case, all of this means that the tests below will be extremely
+ # (excessively, unjustifiably) thorough for scenarios where "the file was
+ # created in the old diff" and then drop off to comparitively lackluster
+ # testing of other scenarios.
+ #
+ # I did still try to cover most of the obvious and potentially tricky
+ # scenarios, though.
+
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:current_user) { project.owner }
+ let(:repository) { project.repository }
+ let(:file_name) { "test-file" }
+ let(:new_file_name) { "#{file_name}-new" }
+ let(:second_file_name) { "#{file_name}-2" }
+ let(:branch_name) { "position-tracer-test" }
+
+ let(:old_diff_refs) { raise NotImplementedError }
+ let(:new_diff_refs) { raise NotImplementedError }
+ let(:old_position) { raise NotImplementedError }
+
+ let(:position_tracer) { described_class.new(repository: project.repository, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) }
+ subject { position_tracer.trace(old_position) }
+
+ def diff_refs(base_commit, head_commit)
+ Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id)
+ end
+
+ def position(attrs = {})
+ attrs.reverse_merge!(
+ diff_refs: old_diff_refs
+ )
+ Gitlab::Diff::Position.new(attrs)
+ end
+
+ def expect_new_position(attrs, new_position = subject)
+ if attrs.nil?
+ expect(new_position).to be_nil
+ else
+ expect(new_position).not_to be_nil
+
+ expect(new_position.diff_refs).to eq(new_diff_refs)
+
+ attrs.each do |attr, value|
+ expect(new_position.send(attr)).to eq(value)
+ end
+ end
+ end
+
+ def create_branch(new_name, branch_name)
+ CreateBranchService.new(project, current_user).execute(new_name, branch_name)
+ end
+
+ def create_file(branch_name, file_name, content)
+ Files::CreateService.new(
+ project,
+ current_user,
+ source_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Create file",
+ file_path: file_name,
+ file_content: content
+ ).execute
+ project.commit(branch_name)
+ end
+
+ def update_file(branch_name, file_name, content)
+ Files::UpdateService.new(
+ project,
+ current_user,
+ source_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Update file",
+ file_path: file_name,
+ file_content: content
+ ).execute
+ project.commit(branch_name)
+ end
+
+ def delete_file(branch_name, file_name)
+ Files::DeleteService.new(
+ project,
+ current_user,
+ source_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Delete file",
+ file_path: file_name
+ ).execute
+ project.commit(branch_name)
+ end
+
+ let(:initial_commit) do
+ create_branch(branch_name, "master")[:branch].name
+ project.commit(branch_name)
+ end
+
+ describe "#trace" do
+ describe "diff scenarios" do
+ let(:create_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ B
+ C
+ CONTENT
+ )
+ end
+
+ let(:create_second_file_commit) do
+ create_file_commit
+
+ create_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ E
+ CONTENT
+ )
+ end
+
+ let(:update_line_commit) do
+ create_second_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ CONTENT
+ )
+ end
+
+ let(:update_second_file_line_commit) do
+ update_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ EE
+ CONTENT
+ )
+ end
+
+ let(:move_line_commit) do
+ update_second_file_line_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ C
+ CONTENT
+ )
+ end
+
+ let(:add_second_file_line_commit) do
+ move_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ EE
+ F
+ CONTENT
+ )
+ end
+
+ let(:move_second_file_line_commit) do
+ add_second_file_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ F
+ EE
+ CONTENT
+ )
+ end
+
+ let(:delete_line_commit) do
+ move_second_file_line_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ CONTENT
+ )
+ end
+
+ let(:delete_second_file_line_commit) do
+ delete_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ F
+ CONTENT
+ )
+ end
+
+ let(:delete_file_commit) do
+ delete_second_file_line_commit
+
+ delete_file(branch_name, file_name)
+ end
+
+ let(:rename_file_commit) do
+ delete_file_commit
+
+ create_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ CONTENT
+ )
+ end
+
+ let(:update_line_again_commit) do
+ rename_file_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ AA
+ CONTENT
+ )
+ end
+
+ let(:move_line_again_commit) do
+ update_line_again_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ AA
+ BB
+ CONTENT
+ )
+ end
+
+ let(:delete_line_again_commit) do
+ move_line_again_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ AA
+ CONTENT
+ )
+ end
+
+ context "when the file was created in the old diff" do
+ context "when the file is created in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is changed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is renamed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 2 A
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: old_position.new_line,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 - A
+ # 2 + AA
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: old_position.new_line,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 + AA
+ # 1 2 BB
+ # 2 - A
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: 1,
+ new_line: 2
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 - A
+ # 2 + AA
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 - BB
+ # 2 - A
+ # 1 + AA
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is deleted in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+ # 3 - C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 - BB
+ # 3 - C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is unchanged in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 B
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 BB
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 BB
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file was changed in the old diff" do
+ context "when the file is created in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+
+ context "when the position pointed at a deleted line in the old diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+
+ context "when the position pointed at an unchanged line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) }
+
+ # old diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is changed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: 1,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: 2,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+ end
+
+ context "when the position pointed at a deleted line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: old_position.old_line,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "typical use scenarios" do
+ let(:second_branch_name) { "#{branch_name}-2" }
+
+ def expect_positions(old_attrs, new_attrs)
+ old_positions = old_attrs.map do |old_attrs|
+ position(old_attrs)
+ end
+
+ new_positions = old_positions.map do |old_position|
+ position_tracer.trace(old_position)
+ end
+
+ new_positions.zip(new_attrs).each do |new_position, new_attrs|
+ expect_new_position(new_attrs, new_position)
+ end
+ end
+
+ let(:create_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ B
+ C
+ D
+ E
+ F
+ CONTENT
+ )
+ end
+
+ let(:second_create_file_commit) do
+ create_file_commit
+
+ create_branch(second_branch_name, branch_name)
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ B
+ C
+ D
+ E
+ F
+ CONTENT
+ )
+ end
+
+ let(:update_file_commit) do
+ second_create_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ C
+ DD
+ E
+ F
+ G
+ CONTENT
+ )
+ end
+
+ let(:update_file_again_commit) do
+ update_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ describe "simple push of new commit" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
+ { old_path: file_name, old_line: 4 }, # - D
+ { new_path: file_name, new_line: 3 }, # + DD
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
+ { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
+ { new_path: file_name, new_line: 6 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
+ { old_path: file_name, old_line: 4, new_line: 4 },
+ nil,
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
+ { old_path: file_name, old_line: 6 },
+ { new_path: file_name, new_line: 7 },
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "force push to overwrite last commit" do
+ let(:second_create_file_commit) do
+ create_file_commit
+
+ create_branch(second_branch_name, branch_name)
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
+ { old_path: file_name, old_line: 4 }, # - D
+ { new_path: file_name, new_line: 3 }, # + DD
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
+ { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
+ { new_path: file_name, new_line: 6 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
+ { old_path: file_name, old_line: 4, new_line: 4 },
+ nil,
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
+ { old_path: file_name, old_line: 6 },
+ { new_path: file_name, new_line: 7 },
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "force push to delete last commit" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ nil,
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 },
+ { old_path: file_name, old_line: 4 },
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 },
+ { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 },
+ nil,
+ { new_path: file_name, new_line: 6 },
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "rebase on top of target branch" do
+ let(:second_update_file_commit) do
+ update_file_commit
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ C
+ DD
+ E
+ F
+ G
+ CONTENT
+ )
+ end
+
+ let(:update_file_again_commit) do
+ second_update_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:overwrite_update_file_again_commit) do
+ update_file_again_commit
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 + Z
+ # 2 + Z
+ # 3 + Z
+ # 1 4 A
+ # 2 - B
+ # 5 + BB
+ # 3 6 C
+ # 4 7 D
+ # 5 8 E
+ # 6 - F
+ # 9 + FF
+ # 0 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 5 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 9 }, # + FF
+ { new_path: file_name, new_line: 10 }, # + G
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "merge of target branch" do
+ let(:merge_commit) do
+ update_file_again_commit
+
+ committer = repository.user_to_committer(current_user)
+
+ options = {
+ message: "Merge branches",
+ author: committer,
+ committer: committer
+ }
+
+ repository.merge(current_user, second_create_file_commit.sha, branch_name, options)
+ project.commit(branch_name)
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 + Z
+ # 2 + Z
+ # 3 + Z
+ # 1 4 A
+ # 2 - B
+ # 5 + BB
+ # 3 6 C
+ # 4 7 D
+ # 5 8 E
+ # 6 - F
+ # 9 + FF
+ # 0 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 5 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 9 }, # + FF
+ { new_path: file_name, new_line: 10 }, # + G
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "changing target branch" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 + BB
+ # 2 3 C
+ # 3 - DD
+ # 4 + D
+ # 4 5 E
+ # 5 - F
+ # 6 + FF
+ # 7 G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 }, # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ nil,
+ { new_path: file_name, new_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 },
+ { new_path: file_name, new_line: 4 },
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 },
+ { old_path: file_name, old_line: 5 },
+ { new_path: file_name, new_line: 6 },
+ { new_path: file_name, new_line: 7 },
+ ]
+
+ expect_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+ 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 b2d7a799810..c19f33e2224 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Email::Message::RepositoryPush do
let!(:author) { create(:author, name: 'Author') }
let(:message) do
- described_class.new(Notify, project.id, 'recipient@example.com', opts)
+ described_class.new(Notify, project.id, opts)
end
context 'new commits have been pushed to repository' do
@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs' do
subject { message.diffs }
- it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) }
+ it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
end
describe '#diffs_count' do
diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb
index 2dc71be0254..252cd4c55c7 100644
--- a/spec/lib/gitlab/fogbugz_import/client_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Gitlab::FogbugzImport::Client, lib: true do
-
let(:client) { described_class.new(uri: '', token: '') }
let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } }
let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } }
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 0a7ca3ec848..0af249d8690 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -33,8 +33,8 @@ describe Gitlab::Gfm::ReferenceRewriter do
end
it { is_expected.to include issue_first.to_reference(new_project) }
- it { is_expected.to_not include issue_second.to_reference(new_project) }
- it { is_expected.to_not include merge_request.to_reference(new_project) }
+ it { is_expected.not_to include issue_second.to_reference(new_project) }
+ it { is_expected.not_to include merge_request.to_reference(new_project) }
end
context 'description ambigous elements' do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index eda956e6f0a..6eca33f9fee 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -32,13 +32,13 @@ describe Gitlab::Gfm::UploadsRewriter do
let(:new_paths) { new_files.map(&:path) }
it 'rewrites content' do
- expect(new_text).to_not eq text
+ expect(new_text).not_to eq text
expect(new_text.length).to eq text.length
end
it 'copies files' do
expect(new_files).to all(exist)
- expect(old_paths).to_not match_array new_paths
+ expect(old_paths).not_to match_array new_paths
expect(old_paths).to all(include(old_project.path_with_namespace))
expect(new_paths).to all(include(new_project.path_with_namespace))
end
@@ -48,8 +48,8 @@ describe Gitlab::Gfm::UploadsRewriter do
end
it 'generates a new secret for each file' do
- expect(new_paths).to_not include image_uploader.secret
- expect(new_paths).to_not include zip_uploader.secret
+ expect(new_paths).not_to include image_uploader.secret
+ expect(new_paths).not_to include zip_uploader.secret
end
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
new file mode 100644
index 00000000000..a15aa173fbd
--- /dev/null
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe Gitlab::Git::Hook, lib: true do
+ describe "#trigger" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ def create_hook(name)
+ FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
+ File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
+ f.write('exit 0')
+ end
+ end
+
+ def create_failing_hook(name)
+ FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
+ File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
+ f.write(<<-HOOK)
+ echo 'regular message from the hook'
+ echo 'error message from the hook' 1>&2
+ exit 1
+ HOOK
+ end
+ end
+
+ ['pre-receive', 'post-receive', 'update'].each do |hook_name|
+
+ context "when triggering a #{hook_name} hook" do
+ context "when the hook is successful" do
+ it "returns success with no errors" do
+ create_hook(hook_name)
+ hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
+ blank = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
+
+ status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ expect(status).to be true
+ expect(errors).to be_blank
+ end
+ end
+
+ context "when the hook is unsuccessful" do
+ it "returns failure with errors" do
+ create_failing_hook(hook_name)
+ hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
+ blank = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
+
+ status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ expect(status).to be false
+ expect(errors).to eq("error message from the hook\n")
+ end
+ end
+ end
+ end
+
+ context "when the hook doesn't exist" do
+ it "returns success with no errors" do
+ hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path)
+ blank = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
+
+ status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
+ expect(status).to be true
+ expect(errors).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 9b3a0e3a75f..c79ba11f782 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
- let(:access) { Gitlab::GitAccess.new(actor, project) }
+ let(:access) { Gitlab::GitAccess.new(actor, project, 'web') }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:actor) { user }
@@ -65,7 +65,43 @@ describe Gitlab::GitAccess, lib: true do
expect(access.can_push_to_branch?(@branch.name)).to be_falsey
end
end
+ end
+
+ describe '#check with single protocols allowed' do
+ def disable_protocol(protocol)
+ settings = ::ApplicationSetting.create_from_defaults
+ settings.update_attribute(:enabled_git_access_protocol, protocol)
+ end
+
+ context 'ssh disabled' do
+ before do
+ disable_protocol('ssh')
+ @acc = Gitlab::GitAccess.new(actor, project, 'ssh')
+ end
+
+ it 'blocks ssh git push' do
+ expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ end
+
+ it 'blocks ssh git pull' do
+ expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ end
+ end
+
+ context 'http disabled' do
+ before do
+ disable_protocol('http')
+ @acc = Gitlab::GitAccess.new(actor, project, 'http')
+ end
+ it 'blocks http push' do
+ expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ end
+
+ it 'blocks http git pull' do
+ expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ end
+ end
end
describe 'download_access_check' do
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 77ecfce6f17..4244b807d41 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do
- let(:access) { Gitlab::GitAccessWiki.new(user, project) }
+ let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') }
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
new file mode 100644
index 00000000000..fc9d5204148
--- /dev/null
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::BranchFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:commit) { create(:commit, project: project) }
+ let(:repo) { double }
+ let(:raw) do
+ {
+ ref: 'feature',
+ repo: repo,
+ sha: commit.id
+ }
+ end
+
+ describe '#exists?' do
+ it 'returns true when both branch, and commit exists' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.exists?).to eq true
+ end
+
+ it 'returns false when branch does not exist' do
+ branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
+
+ expect(branch.exists?).to eq false
+ end
+
+ it 'returns false when commit does not exist' do
+ branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
+
+ expect(branch.exists?).to eq false
+ end
+ end
+
+ describe '#name' do
+ it 'returns raw ref when branch exists' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.name).to eq 'feature'
+ end
+
+ it 'returns formatted ref when branch does not exist' do
+ branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
+
+ expect(branch.name).to eq 'removed-branch-2e5d3239'
+ end
+ end
+
+ describe '#repo' do
+ it 'returns raw repo' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.repo).to eq repo
+ end
+ end
+
+ describe '#sha' do
+ it 'returns raw sha' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.sha).to eq commit.id
+ end
+ end
+
+ describe '#valid?' do
+ it 'returns true when raw repo is present' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.valid?).to eq true
+ end
+
+ it 'returns false when raw repo is blank' do
+ branch = described_class.new(project, double(raw.merge(repo: nil)))
+
+ expect(branch.valid?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 7c21cbe96d9..3b023a35446 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -20,6 +20,20 @@ describe Gitlab::GithubImport::Client, lib: true do
expect { client.api }.not_to raise_error
end
+ context 'when config is missing' do
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ end
+
+ it 'is still possible to get an Octokit client' do
+ expect { client.api }.not_to raise_error
+ end
+
+ it 'is not be possible to get an OAuth2 client' do
+ expect { client.client }.to raise_error(Projects::ImportService::Error)
+ end
+ end
+
context 'allow SSL verification to be configurable on API' do
before do
github_provider['verify_ssl'] = false
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index 55e86d4ceac..9ae02a6c45f 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -29,6 +29,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
commit_id: nil,
line_code: nil,
author_id: project.creator_id,
+ type: nil,
created_at: created_at,
updated_at: updated_at
}
@@ -56,6 +57,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3',
author_id: project.creator_id,
+ type: 'LegacyDiffNote',
created_at: created_at,
updated_at: updated_at
}
diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
new file mode 100644
index 00000000000..110ba428258
--- /dev/null
+++ b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::HookFormatter, lib: true do
+ describe '#id' do
+ it 'returns raw id' do
+ raw = double(id: 100000)
+ formatter = described_class.new(raw)
+ expect(formatter.id).to eq 100000
+ end
+ end
+
+ describe '#name' do
+ it 'returns raw id' do
+ raw = double(name: 'web')
+ formatter = described_class.new(raw)
+ expect(formatter.name).to eq 'web'
+ end
+ end
+
+ describe '#config' do
+ it 'returns raw config.attrs' do
+ raw = double(config: double(attrs: { url: 'http://something.com/webhook' }))
+ formatter = described_class.new(raw)
+ expect(formatter.config).to eq({ url: 'http://something.com/webhook' })
+ end
+ end
+
+ describe '#valid?' do
+ it 'returns true when events contains the wildcard event' do
+ raw = double(events: ['*', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains the create event' do
+ raw = double(events: ['create', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains delete event' do
+ raw = double(events: ['delete', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains pull_request event' do
+ raw = double(events: ['pull_request', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns false when events does not contains branch related events' do
+ raw = double(events: ['member', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq false
+ end
+
+ it 'returns false when hook is not active' do
+ raw = double(events: ['pull_request', 'commit_comment'], active: false)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index e94440a7fb0..87593e32db0 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Gitlab::GithubImport::LabelFormatter, lib: true do
-
describe '#attributes' do
it 'returns formatted attributes' do
project = create(:project)
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index e59c0ca110e..79931ecd134 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -2,11 +2,13 @@ require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:project) { create(:project) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:repository) { double(id: 1, fork: false) }
let(:source_repo) { repository }
- let(:source_branch) { double(ref: 'feature', repo: source_repo) }
+ let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
let(:target_repo) { repository }
- let(:target_branch) { double(ref: 'master', repo: target_repo) }
+ let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -41,8 +43,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
source_branch: 'feature',
+ source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
+ target_branch_sha: target_sha,
state: 'opened',
milestone: nil,
author_id: project.creator_id,
@@ -66,8 +70,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
source_branch: 'feature',
+ source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
+ target_branch_sha: target_sha,
state: 'closed',
milestone: nil,
author_id: project.creator_id,
@@ -91,8 +97,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
source_branch: 'feature',
+ source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
+ target_branch_sha: target_sha,
state: 'merged',
milestone: nil,
author_id: project.creator_id,
@@ -137,11 +145,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:milestone) { double(number: 45) }
let(:raw_data) { double(base_data.merge(milestone: milestone)) }
- it 'returns nil when milestone does not exists' do
+ it 'returns nil when milestone does not exist' do
expect(pull_request.attributes.fetch(:milestone)).to be_nil
end
- it 'returns milestone when is exists' do
+ it 'returns milestone when it exists' do
milestone = create(:milestone, project: project, iid: 45)
expect(pull_request.attributes.fetch(:milestone)).to eq milestone
@@ -158,36 +166,16 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
describe '#valid?' do
- let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object }
-
- context 'when source, and target repositories are the same' do
- context 'and source and target branches exists' do
- let(:raw_data) { double(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) { double(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) { double(base_data.merge(head: source_branch, base: invalid_branch)) }
+ context 'when source, and target repos are not a fork' do
+ let(:raw_data) { double(base_data) }
- it 'returns false' do
- expect(pull_request.valid?).to eq false
- end
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
end
end
context 'when source repo is a fork' do
- let(:source_repo) { double(id: 2, fork: true) }
+ let(:source_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
it 'returns false' do
@@ -196,7 +184,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
context 'when target repo is a fork' do
- let(:target_repo) { double(id: 2, fork: true) }
+ let(:target_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
it 'returns false' do
diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
index e6831e7c383..cd8e805466a 100644
--- a/spec/lib/gitlab/gitlab_import/client_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
describe Gitlab::GitlabImport::Client, lib: true do
+ include ImportSpecHelper
+
let(:token) { '123456' }
let(:client) { Gitlab::GitlabImport::Client.new(token) }
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab")
+ stub_omniauth_provider('gitlab')
end
it 'all OAuth2 client options are symbols' do
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 647631271e0..54f85f8cffc 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -19,7 +19,6 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
end
describe "#execute" do
-
it "imports status labels" do
subject.execute
diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb
new file mode 100644
index 00000000000..f5c064303ad
--- /dev/null
+++ b/spec/lib/gitlab/graphs/commits_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Graphs::Commits, lib: true do
+ let!(:project) { create(:project, :public, :empty_repo) }
+
+ let!(:commit1) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: Time.now) }
+ let!(:commit1_yesterday) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: 1.day.ago)}
+
+ let!(:commit2) { create(:commit, git_commit: RepoHelpers.another_sample_commit, project: project, committed_date: Time.now) }
+
+ describe '#commit_per_day' do
+ context 'when range is only commits from today' do
+ subject { described_class.new([commit2, commit1]).commit_per_day }
+ it { is_expected.to eq 2 }
+ end
+ end
+
+ context 'when range is only commits from today' do
+ subject { described_class.new([commit2, commit1]) }
+ describe '#commit_per_day' do
+ it { expect(subject.commit_per_day).to eq 2 }
+ end
+
+ describe '#duration' do
+ it { expect(subject.duration).to eq 0 }
+ end
+ end
+
+ context 'with commits from yesterday and today' do
+ subject { described_class.new([commit2, commit1_yesterday]) }
+ describe '#commit_per_day' do
+ it { expect(subject.commit_per_day).to eq 1 }
+ end
+
+ describe '#duration' do
+ it { expect(subject.duration).to eq 1 }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 1620eb6c60a..364532e94e3 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::Highlight, lib: true do
include RepoHelpers
let(:project) { create(:project) }
+ let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
describe '.highlight_lines' do
@@ -18,4 +19,30 @@ describe Gitlab::Highlight, lib: true do
end
end
+ describe 'custom highlighting from .gitattributes' do
+ let(:branch) { 'gitattributes' }
+ let(:blob) { repository.blob_at_branch(branch, path) }
+
+ let(:highlighter) do
+ Gitlab::Highlight.new(blob.path, blob.data, repository: repository)
+ end
+
+ before { project.change_head('gitattributes') }
+
+ describe 'basic language selection' do
+ let(:path) { 'custom-highlighting/test.gitlab-custom' }
+ it 'highlights as ruby' do
+ expect(highlighter.lexer.tag).to eq 'ruby'
+ end
+ end
+
+ describe 'cgi options' do
+ let(:path) { 'custom-highlighting/test.gitlab-cgi' }
+
+ it 'highlights as json with erb' do
+ expect(highlighter.lexer.tag).to eq 'erb'
+ expect(highlighter.lexer.parent.tag).to eq 'json'
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
new file mode 100644
index 00000000000..6d5aa0d04a2
--- /dev/null
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::MembersMapper, services: true do
+ describe 'map members' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:user2) { create(:user) }
+ let(:exported_user_id) { 99 }
+ let(:exported_members) do
+ [{
+ "id" => 2,
+ "access_level" => 40,
+ "source_id" => 14,
+ "source_type" => "Project",
+ "user_id" => 19,
+ "notification_level" => 3,
+ "created_at" => "2016-03-11T10:21:44.822Z",
+ "updated_at" => "2016-03-11T10:21:44.822Z",
+ "created_by_id" => nil,
+ "invite_email" => nil,
+ "invite_token" => nil,
+ "invite_accepted_at" => nil,
+ "user" =>
+ {
+ "id" => exported_user_id,
+ "email" => user2.email,
+ "username" => user2.username
+ }
+ }]
+ end
+
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user, project: project)
+ end
+
+ it 'maps a project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+
+ it 'defaults to importer project member if it does not exist' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+
+ it 'updates missing author IDs on missing project member' do
+ members_mapper.map[-1]
+
+ expect(members_mapper.missing_author_ids.first).to eq(-1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
new file mode 100644
index 00000000000..0b30e8c9b04
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.json
@@ -0,0 +1,7382 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "visibility_level": 10,
+ "archived": false,
+ "issues": [
+ {
+ "id": 40,
+ "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.",
+ "assignee_id": 1,
+ "author_id": 22,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:08.340Z",
+ "updated_at": "2016-06-14T15:02:47.967Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Aliquam enim illo et possimus.",
+ "milestone_id": 18,
+ "state": "opened",
+ "iid": 10,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 351,
+ "note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:47.770Z",
+ "updated_at": "2016-06-14T15:02:47.770Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 352,
+ "note": "Est reprehenderit quas aut aspernatur autem recusandae voluptatem.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:47.795Z",
+ "updated_at": "2016-06-14T15:02:47.795Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 353,
+ "note": "Perspiciatis suscipit voluptates in eius nihil.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:47.823Z",
+ "updated_at": "2016-06-14T15:02:47.823Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 354,
+ "note": "Aut vel voluptas corrupti nisi provident laboriosam magnam aut.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:47.850Z",
+ "updated_at": "2016-06-14T15:02:47.850Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 355,
+ "note": "Officia dolore consequatur in saepe cum magni.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:47.876Z",
+ "updated_at": "2016-06-14T15:02:47.876Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 356,
+ "note": "Cum ipsum rem voluptas eaque et ea.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:47.908Z",
+ "updated_at": "2016-06-14T15:02:47.908Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 357,
+ "note": "Recusandae excepturi asperiores suscipit autem nostrum.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:47.937Z",
+ "updated_at": "2016-06-14T15:02:47.937Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 358,
+ "note": "Et hic est id similique et non nesciunt voluptate.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:47.965Z",
+ "updated_at": "2016-06-14T15:02:47.965Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "title": "Delectus veniam ratione in eos culpa et natus molestiae earum aut.",
+ "assignee_id": 20,
+ "author_id": 22,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:08.233Z",
+ "updated_at": "2016-06-14T15:02:48.194Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.",
+ "milestone_id": 16,
+ "state": "opened",
+ "iid": 9,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 359,
+ "note": "Quo eius velit quia et id quam.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:48.009Z",
+ "updated_at": "2016-06-14T15:02:48.009Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 360,
+ "note": "Nulla commodi ratione cumque id autem.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:48.032Z",
+ "updated_at": "2016-06-14T15:02:48.032Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 361,
+ "note": "Illum non ea sed dolores corrupti.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:48.056Z",
+ "updated_at": "2016-06-14T15:02:48.056Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 362,
+ "note": "Facere dolores ipsum dolorum maiores omnis occaecati ab.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:48.082Z",
+ "updated_at": "2016-06-14T15:02:48.082Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 363,
+ "note": "Quod laudantium similique sint aut est ducimus.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:48.113Z",
+ "updated_at": "2016-06-14T15:02:48.113Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 364,
+ "note": "Aut omnis eos esse incidunt vero reiciendis.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:48.139Z",
+ "updated_at": "2016-06-14T15:02:48.139Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 365,
+ "note": "Beatae dolore et doloremque asperiores sunt.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:48.162Z",
+ "updated_at": "2016-06-14T15:02:48.162Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 366,
+ "note": "Doloribus ipsam ex delectus rerum libero recusandae modi repellendus.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:48.192Z",
+ "updated_at": "2016-06-14T15:02:48.192Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 38,
+ "title": "Quasi adipisci non cupiditate dolorem quo qui earum sed.",
+ "assignee_id": 1,
+ "author_id": 6,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:08.154Z",
+ "updated_at": "2016-06-14T15:02:48.614Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Ea recusandae neque autem tempora.",
+ "milestone_id": 16,
+ "state": "closed",
+ "iid": 8,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 367,
+ "note": "Accusantium fugiat et eaque quisquam esse corporis.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:48.235Z",
+ "updated_at": "2016-06-14T15:02:48.235Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 368,
+ "note": "Ea labore eum nam qui laboriosam.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:48.261Z",
+ "updated_at": "2016-06-14T15:02:48.261Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 369,
+ "note": "Accusantium quis sed molestiae et.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:48.294Z",
+ "updated_at": "2016-06-14T15:02:48.294Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 370,
+ "note": "Corporis numquam a voluptatem pariatur asperiores dolorem delectus autem.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:48.523Z",
+ "updated_at": "2016-06-14T15:02:48.523Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 371,
+ "note": "Ea accusantium maxime voluptas rerum.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:48.546Z",
+ "updated_at": "2016-06-14T15:02:48.546Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 372,
+ "note": "Pariatur iusto et et excepturi similique ipsam eum.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:48.569Z",
+ "updated_at": "2016-06-14T15:02:48.569Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 373,
+ "note": "Aliquam et culpa officia iste eius.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:48.591Z",
+ "updated_at": "2016-06-14T15:02:48.591Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 374,
+ "note": "Ab id velit id unde laborum.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:48.613Z",
+ "updated_at": "2016-06-14T15:02:48.613Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "title": "Cupiditate quo aut ducimus minima molestiae vero numquam possimus.",
+ "assignee_id": 15,
+ "author_id": 20,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:08.051Z",
+ "updated_at": "2016-06-14T15:02:48.854Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Maiores architecto quos in dolorem.",
+ "milestone_id": 17,
+ "state": "opened",
+ "iid": 7,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 375,
+ "note": "Quasi fugit qui sed eligendi aut quia.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:48.647Z",
+ "updated_at": "2016-06-14T15:02:48.647Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 376,
+ "note": "Esse nesciunt voluptatem ex vero est consequatur.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:48.674Z",
+ "updated_at": "2016-06-14T15:02:48.674Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 377,
+ "note": "Similique qui quas non aut et velit sequi in.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:48.696Z",
+ "updated_at": "2016-06-14T15:02:48.696Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 378,
+ "note": "Eveniet ut cupiditate repellendus numquam in esse eius.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:48.720Z",
+ "updated_at": "2016-06-14T15:02:48.720Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 379,
+ "note": "Velit est dolorem adipisci rerum sed iure.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:48.755Z",
+ "updated_at": "2016-06-14T15:02:48.755Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 380,
+ "note": "Voluptatem ullam ab ut illo ut quo.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:48.793Z",
+ "updated_at": "2016-06-14T15:02:48.793Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 381,
+ "note": "Voluptatem impedit beatae quasi ipsa earum consectetur.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:48.823Z",
+ "updated_at": "2016-06-14T15:02:48.823Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 382,
+ "note": "Nihil officiis eaque incidunt sunt voluptatum excepturi.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:48.852Z",
+ "updated_at": "2016-06-14T15:02:48.852Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 36,
+ "title": "Necessitatibus dolor est enim quia rem suscipit quidem voluptas ullam.",
+ "assignee_id": 20,
+ "author_id": 16,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.958Z",
+ "updated_at": "2016-06-14T15:02:49.044Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Ut aut ut et tenetur velit aut id modi.",
+ "milestone_id": 16,
+ "state": "opened",
+ "iid": 6,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 383,
+ "note": "Excepturi deleniti sunt rerum nesciunt vero fugiat possimus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:48.885Z",
+ "updated_at": "2016-06-14T15:02:48.885Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 384,
+ "note": "Et est nemo sed nam sed.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:48.910Z",
+ "updated_at": "2016-06-14T15:02:48.910Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 385,
+ "note": "Animi mollitia nulla facere amet aut quaerat.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:48.934Z",
+ "updated_at": "2016-06-14T15:02:48.934Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 386,
+ "note": "Excepturi id voluptas ut odio officiis omnis.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:48.955Z",
+ "updated_at": "2016-06-14T15:02:48.955Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 387,
+ "note": "Molestiae labore officiis magni et eligendi quasi maxime.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:48.978Z",
+ "updated_at": "2016-06-14T15:02:48.978Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 388,
+ "note": "Officia tenetur praesentium rem nam non.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:49.001Z",
+ "updated_at": "2016-06-14T15:02:49.001Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 389,
+ "note": "Et et et molestiae reprehenderit.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:49.022Z",
+ "updated_at": "2016-06-14T15:02:49.022Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 390,
+ "note": "Aperiam in consequatur est sunt cum quia.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:49.043Z",
+ "updated_at": "2016-06-14T15:02:49.043Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 35,
+ "title": "Repellat praesentium deserunt maxime incidunt harum porro qui.",
+ "assignee_id": 6,
+ "author_id": 20,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.832Z",
+ "updated_at": "2016-06-14T15:02:49.226Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Dicta nisi nihil non ipsa velit.",
+ "milestone_id": 20,
+ "state": "closed",
+ "iid": 5,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 391,
+ "note": "Qui magnam et assumenda quod id dicta necessitatibus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:49.075Z",
+ "updated_at": "2016-06-14T15:02:49.075Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 392,
+ "note": "Consectetur deserunt possimus dolor est odio.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:49.095Z",
+ "updated_at": "2016-06-14T15:02:49.095Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 393,
+ "note": "Labore nisi quo cumque voluptas consequatur aut qui.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:49.117Z",
+ "updated_at": "2016-06-14T15:02:49.117Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 394,
+ "note": "Et totam facilis voluptas et enim.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:49.138Z",
+ "updated_at": "2016-06-14T15:02:49.138Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 395,
+ "note": "Ratione sint pariatur sed omnis eligendi quo libero exercitationem.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:49.160Z",
+ "updated_at": "2016-06-14T15:02:49.160Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 396,
+ "note": "Iure hic autem id voluptatem.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:49.182Z",
+ "updated_at": "2016-06-14T15:02:49.182Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 397,
+ "note": "Excepturi eum laboriosam delectus repellendus odio nisi et voluptatem.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:49.205Z",
+ "updated_at": "2016-06-14T15:02:49.205Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 398,
+ "note": "Ut quis ex soluta consequatur et blanditiis.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:49.225Z",
+ "updated_at": "2016-06-14T15:02:49.225Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 34,
+ "title": "Ullam expedita deserunt libero consequatur quia dolor harum perferendis facere quidem.",
+ "assignee_id": 20,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.717Z",
+ "updated_at": "2016-06-14T15:02:49.416Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Ut et explicabo vel voluptatem consequuntur ut sed.",
+ "milestone_id": 19,
+ "state": "closed",
+ "iid": 4,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 399,
+ "note": "Dolor iste tempora tenetur non vitae maiores voluptatibus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:49.256Z",
+ "updated_at": "2016-06-14T15:02:49.256Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 400,
+ "note": "Aut sit quidem qui adipisci maxime excepturi iusto.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:49.284Z",
+ "updated_at": "2016-06-14T15:02:49.284Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 401,
+ "note": "Et a necessitatibus autem quidem animi sunt voluptatum rerum.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:49.305Z",
+ "updated_at": "2016-06-14T15:02:49.305Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 402,
+ "note": "Esse laboriosam quo voluptatem quis molestiae.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:49.328Z",
+ "updated_at": "2016-06-14T15:02:49.328Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 403,
+ "note": "Nemo magnam distinctio est ut voluptate ea.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:49.350Z",
+ "updated_at": "2016-06-14T15:02:49.350Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 404,
+ "note": "Omnis sed rerum neque rerum quae quam nulla officiis.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:49.372Z",
+ "updated_at": "2016-06-14T15:02:49.372Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 405,
+ "note": "Quo soluta dolorem vitae ad consequatur qui aut dicta.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:49.394Z",
+ "updated_at": "2016-06-14T15:02:49.394Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 406,
+ "note": "Magni minus est aut aut totam ut.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:49.414Z",
+ "updated_at": "2016-06-14T15:02:49.414Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 33,
+ "title": "Numquam accusamus eos iste exercitationem magni non inventore.",
+ "assignee_id": 15,
+ "author_id": 26,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.611Z",
+ "updated_at": "2016-06-14T15:02:49.661Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Non asperiores velit accusantium voluptate.",
+ "milestone_id": 18,
+ "state": "closed",
+ "iid": 3,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 407,
+ "note": "Quod ea et possimus architecto.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:49.450Z",
+ "updated_at": "2016-06-14T15:02:49.450Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 408,
+ "note": "Reiciendis est et unde perferendis dicta ut praesentium quasi.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:49.503Z",
+ "updated_at": "2016-06-14T15:02:49.503Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 409,
+ "note": "Magni quia odio blanditiis pariatur voluptas.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:49.527Z",
+ "updated_at": "2016-06-14T15:02:49.527Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 410,
+ "note": "Enim quam ut et et et.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:49.551Z",
+ "updated_at": "2016-06-14T15:02:49.551Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 411,
+ "note": "Fugit voluptatem ratione maxime expedita.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:49.578Z",
+ "updated_at": "2016-06-14T15:02:49.578Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 412,
+ "note": "Voluptatem enim aut ipsa et et ducimus.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:49.604Z",
+ "updated_at": "2016-06-14T15:02:49.604Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 413,
+ "note": "Quia repellat fugiat consectetur quidem.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:49.631Z",
+ "updated_at": "2016-06-14T15:02:49.631Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 414,
+ "note": "Corporis ipsum et ea necessitatibus quod assumenda repudiandae quam.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:49.659Z",
+ "updated_at": "2016-06-14T15:02:49.659Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 32,
+ "title": "Necessitatibus magnam qui at velit consequatur perspiciatis.",
+ "assignee_id": 22,
+ "author_id": 15,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.431Z",
+ "updated_at": "2016-06-14T15:02:49.884Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Molestiae corporis magnam et fugit aliquid nulla quia.",
+ "milestone_id": 17,
+ "state": "closed",
+ "iid": 2,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 415,
+ "note": "Nemo consequatur sed blanditiis qui id iure dolores.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:49.694Z",
+ "updated_at": "2016-06-14T15:02:49.694Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 416,
+ "note": "Voluptas ab accusantium dicta in.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:49.718Z",
+ "updated_at": "2016-06-14T15:02:49.718Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 417,
+ "note": "Esse odit qui a et eum ducimus.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:49.741Z",
+ "updated_at": "2016-06-14T15:02:49.741Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 418,
+ "note": "Sequi dolor doloribus ratione placeat repellendus.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:49.767Z",
+ "updated_at": "2016-06-14T15:02:49.767Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 419,
+ "note": "Quae aspernatur rem est similique.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:49.796Z",
+ "updated_at": "2016-06-14T15:02:49.796Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 420,
+ "note": "Voluptate omnis et id rerum non nesciunt laudantium assumenda.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:49.825Z",
+ "updated_at": "2016-06-14T15:02:49.825Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 421,
+ "note": "Quia enim ab et eligendi.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:49.853Z",
+ "updated_at": "2016-06-14T15:02:49.853Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 422,
+ "note": "In fugiat rerum voluptas quas officia.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:49.881Z",
+ "updated_at": "2016-06-14T15:02:49.881Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ },
+ {
+ "id": 31,
+ "title": "Libero nam magnam incidunt eaque placeat error et.",
+ "assignee_id": 1,
+ "author_id": 16,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.280Z",
+ "updated_at": "2016-06-14T15:02:50.134Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Quod ad architecto qui est sed quia.",
+ "milestone_id": 20,
+ "state": "closed",
+ "iid": 1,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "notes": [
+ {
+ "id": 423,
+ "note": "A mollitia qui iste consequatur eaque iure omnis sunt.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:49.933Z",
+ "updated_at": "2016-06-14T15:02:49.933Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 424,
+ "note": "Eveniet est et blanditiis sequi alias.",
+ "noteable_type": "Issue",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:49.965Z",
+ "updated_at": "2016-06-14T15:02:49.965Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 425,
+ "note": "Commodi tempore voluptas doloremque est.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:49.996Z",
+ "updated_at": "2016-06-14T15:02:49.996Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 426,
+ "note": "Quo libero impedit odio debitis rerum aspernatur.",
+ "noteable_type": "Issue",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:50.024Z",
+ "updated_at": "2016-06-14T15:02:50.024Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 427,
+ "note": "Dolorem voluptatem qui labore deserunt.",
+ "noteable_type": "Issue",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:50.049Z",
+ "updated_at": "2016-06-14T15:02:50.049Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 428,
+ "note": "Est blanditiis laboriosam enim ipsam.",
+ "noteable_type": "Issue",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:50.077Z",
+ "updated_at": "2016-06-14T15:02:50.077Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 429,
+ "note": "Et in voluptatem animi dolorem eos.",
+ "noteable_type": "Issue",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:50.107Z",
+ "updated_at": "2016-06-14T15:02:50.107Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 430,
+ "note": "Unde culpa voluptate qui sint quos.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:50.132Z",
+ "updated_at": "2016-06-14T15:02:50.132Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ]
+ }
+ ],
+ "labels": [
+
+ ],
+ "milestones": [
+ {
+ "id": 20,
+ "title": "v4.0",
+ "project_id": 5,
+ "description": "Totam quam laborum id magnam natus eaque aspernatur.",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.590Z",
+ "updated_at": "2016-06-14T15:02:04.590Z",
+ "state": "active",
+ "iid": 5,
+ "events": [
+ {
+ "id": 240,
+ "target_type": "Milestone",
+ "target_id": 20,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:04.593Z",
+ "updated_at": "2016-06-14T15:02:04.593Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 60,
+ "target_type": "Milestone",
+ "target_id": 20,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:04.593Z",
+ "updated_at": "2016-06-14T15:02:04.593Z",
+ "action": 1,
+ "author_id": 20
+ }
+ ]
+ },
+ {
+ "id": 19,
+ "title": "v3.0",
+ "project_id": 5,
+ "description": "Rerum at autem exercitationem ea voluptates harum quam placeat.",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.583Z",
+ "updated_at": "2016-06-14T15:02:04.583Z",
+ "state": "active",
+ "iid": 4,
+ "events": [
+ {
+ "id": 241,
+ "target_type": "Milestone",
+ "target_id": 19,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:04.585Z",
+ "updated_at": "2016-06-14T15:02:04.585Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 59,
+ "target_type": "Milestone",
+ "target_id": 19,
+ "title": null,
+ "data": {
+ "object_kind": "push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
+ "ref": "refs/heads/removable-group-owner",
+ "checkout_sha": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
+ "message": null,
+ "user_id": 273486,
+ "user_name": "James Lopez",
+ "user_email": "james@jameslopez.es",
+ "project_id": 562317,
+ "repository": {
+ "name": "GitLab Community Edition",
+ "url": "git@gitlab.com:james11/gitlab-ce.git",
+ "description": "Version Control on your Server. See http://gitlab.org/gitlab-ce/ and the README for more information",
+ "homepage": "https://gitlab.com/james11/gitlab-ce",
+ "git_http_url": "https://gitlab.com/james11/gitlab-ce.git",
+ "git_ssh_url": "git@gitlab.com:james11/gitlab-ce.git",
+ "visibility_level": 20
+ },
+ "commits": [
+ {
+ "id": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
+ "message": "fixed last group owner issue and added test\\n",
+ "timestamp": "2015-10-29T16:10:27+00:00",
+ "url": "https://gitlab.com/james11/gitlab-ce/commit/de990aa15829d0ab182ad5a55b4c527846c0d39c",
+ "author": {
+ "name": "James Lopez",
+ "email": "james.lopez@vodafone.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
+ },
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:04.585Z",
+ "updated_at": "2016-06-14T15:02:04.585Z",
+ "action": 1,
+ "author_id": 25
+ }
+ ]
+ },
+ {
+ "id": 18,
+ "title": "v2.0",
+ "project_id": 5,
+ "description": "Error dolorem rerum aut nulla.",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.576Z",
+ "updated_at": "2016-06-14T15:02:04.576Z",
+ "state": "active",
+ "iid": 3,
+ "events": [
+ {
+ "id": 242,
+ "target_type": "Milestone",
+ "target_id": 18,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:04.579Z",
+ "updated_at": "2016-06-14T15:02:04.579Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 58,
+ "target_type": "Milestone",
+ "target_id": 18,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:04.579Z",
+ "updated_at": "2016-06-14T15:02:04.579Z",
+ "action": 1,
+ "author_id": 22
+ }
+ ]
+ },
+ {
+ "id": 17,
+ "title": "v1.0",
+ "project_id": 5,
+ "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.569Z",
+ "updated_at": "2016-06-14T15:02:04.569Z",
+ "state": "active",
+ "iid": 2,
+ "events": [
+ {
+ "id": 243,
+ "target_type": "Milestone",
+ "target_id": 17,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:04.570Z",
+ "updated_at": "2016-06-14T15:02:04.570Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 57,
+ "target_type": "Milestone",
+ "target_id": 17,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:04.570Z",
+ "updated_at": "2016-06-14T15:02:04.570Z",
+ "action": 1,
+ "author_id": 20
+ }
+ ]
+ },
+ {
+ "id": 16,
+ "title": "v0.0",
+ "project_id": 5,
+ "description": "Velit numquam et sed sit.",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.561Z",
+ "updated_at": "2016-06-14T15:02:04.561Z",
+ "state": "closed",
+ "iid": 1,
+ "events": [
+ {
+ "id": 244,
+ "target_type": "Milestone",
+ "target_id": 16,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:04.563Z",
+ "updated_at": "2016-06-14T15:02:04.563Z",
+ "action": 1,
+ "author_id": 26
+ },
+ {
+ "id": 56,
+ "target_type": "Milestone",
+ "target_id": 16,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:04.563Z",
+ "updated_at": "2016-06-14T15:02:04.563Z",
+ "action": 1,
+ "author_id": 26
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "releases": [
+
+ ],
+ "project_members": [
+ {
+ "id": 36,
+ "access_level": 40,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 16,
+ "notification_level": 3,
+ "created_at": "2016-06-14T15:02:03.834Z",
+ "updated_at": "2016-06-14T15:02:03.834Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "requested_at": null,
+ "user": {
+ "id": 16,
+ "email": "maritza_schoen@block.ca",
+ "username": "bernard_willms"
+ }
+ },
+ {
+ "id": 35,
+ "access_level": 10,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 6,
+ "notification_level": 3,
+ "created_at": "2016-06-14T15:02:03.811Z",
+ "updated_at": "2016-06-14T15:02:03.811Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "requested_at": null,
+ "user": {
+ "id": 6,
+ "email": "shaina@koelpindenesik.com",
+ "username": "saul_will"
+ }
+ },
+ {
+ "id": 34,
+ "access_level": 20,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 15,
+ "notification_level": 3,
+ "created_at": "2016-06-14T15:02:03.776Z",
+ "updated_at": "2016-06-14T15:02:03.776Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "requested_at": null,
+ "user": {
+ "id": 15,
+ "email": "breanna_sanford@wolf.com",
+ "username": "emmet.schamberger"
+ }
+ },
+ {
+ "id": 33,
+ "access_level": 20,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 26,
+ "notification_level": 3,
+ "created_at": "2016-06-14T15:02:03.742Z",
+ "updated_at": "2016-06-14T15:02:03.742Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "requested_at": null,
+ "user": {
+ "id": 26,
+ "email": "user4@example.com",
+ "username": "user4"
+ }
+ }
+ ],
+ "merge_requests": [
+ {
+ "id": 27,
+ "target_branch": "feature",
+ "source_branch": "feature_conflict",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": null,
+ "title": "Cannot be automatically merged",
+ "created_at": "2016-06-14T15:02:36.568Z",
+ "updated_at": "2016-06-14T15:02:56.815Z",
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 9,
+ "description": null,
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 671,
+ "note": "Sit voluptatibus eveniet architecto quidem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:56.632Z",
+ "updated_at": "2016-06-14T15:02:56.632Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 672,
+ "note": "Odio maxime ratione voluptatibus sed.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:56.656Z",
+ "updated_at": "2016-06-14T15:02:56.656Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 673,
+ "note": "Et deserunt et omnis nihil excepturi accusantium.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:56.679Z",
+ "updated_at": "2016-06-14T15:02:56.679Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 674,
+ "note": "Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:56.700Z",
+ "updated_at": "2016-06-14T15:02:56.700Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 675,
+ "note": "Numquam est at dolor quo et sed eligendi similique.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:56.720Z",
+ "updated_at": "2016-06-14T15:02:56.720Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 676,
+ "note": "Et perferendis aliquam sunt nisi labore delectus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:56.742Z",
+ "updated_at": "2016-06-14T15:02:56.742Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 677,
+ "note": "Aut ex rerum et in.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:56.791Z",
+ "updated_at": "2016-06-14T15:02:56.791Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 678,
+ "note": "Dolor laborum earum ut exercitationem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:56.814Z",
+ "updated_at": "2016-06-14T15:02:56.814Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 27,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 27,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
+ "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2014-08-06T08:35:52.000+02:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-08-06T08:35:52.000+02:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ ],
+ "authored_date": "2014-02-27T10:01:38.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ ],
+ "authored_date": "2014-02-27T09:57:31.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "d14d6c0abdd253381df51a723d58691b2ee1ab08"
+ ],
+ "authored_date": "2014-02-27T09:54:21.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:54:21.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
+ ],
+ "authored_date": "2014-02-27T09:49:50.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:49:50.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:48:32.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:48:32.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "new_path": ".DS_Store",
+ "old_path": ".DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "new_path": ".gitignore",
+ "old_path": ".gitignore",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "new_path": ".gitmodules",
+ "old_path": ".gitmodules",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "new_path": "files/.DS_Store",
+ "old_path": "files/.DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n",
+ "new_path": "files/ruby/feature.rb",
+ "old_path": "files/ruby/feature.rb",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "new_path": "files/ruby/popen.rb",
+ "old_path": "files/ruby/popen.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "new_path": "files/ruby/regex.rb",
+ "old_path": "files/ruby/regex.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "new_path": "gitlab-grack",
+ "old_path": "gitlab-grack",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "new_path": "gitlab-shell",
+ "old_path": "gitlab-shell",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 27,
+ "created_at": "2016-06-14T15:02:36.572Z",
+ "updated_at": "2016-06-14T15:02:36.658Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "9"
+ },
+ "events": [
+ {
+ "id": 221,
+ "target_type": "MergeRequest",
+ "target_id": 27,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:36.703Z",
+ "updated_at": "2016-06-14T15:02:36.703Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 187,
+ "target_type": "MergeRequest",
+ "target_id": 27,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:36.703Z",
+ "updated_at": "2016-06-14T15:02:36.703Z",
+ "action": 1,
+ "author_id": 1
+ }
+ ]
+ },
+ {
+ "id": 26,
+ "target_branch": "master",
+ "source_branch": "feature",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": null,
+ "title": "Can be automatically merged",
+ "created_at": "2016-06-14T15:02:36.418Z",
+ "updated_at": "2016-06-14T15:02:57.013Z",
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 8,
+ "description": null,
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 679,
+ "note": "Qui rerum totam nisi est.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:56.848Z",
+ "updated_at": "2016-06-14T15:02:56.848Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 680,
+ "note": "Pariatur magni corrupti consequatur debitis minima error beatae voluptatem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:56.871Z",
+ "updated_at": "2016-06-14T15:02:56.871Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 681,
+ "note": "Qui quis ut modi eos rerum ratione.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:56.895Z",
+ "updated_at": "2016-06-14T15:02:56.895Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 682,
+ "note": "Illum quidem expedita mollitia fugit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:56.918Z",
+ "updated_at": "2016-06-14T15:02:56.918Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 683,
+ "note": "Consectetur voluptate sit sint possimus veritatis quod.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:56.942Z",
+ "updated_at": "2016-06-14T15:02:56.942Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 684,
+ "note": "Natus libero quibusdam rem assumenda deleniti accusamus sed earum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:56.966Z",
+ "updated_at": "2016-06-14T15:02:56.966Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 685,
+ "note": "Tenetur autem nihil rerum odit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:56.989Z",
+ "updated_at": "2016-06-14T15:02:56.989Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 686,
+ "note": "Quia maiores et odio sed.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:57.012Z",
+ "updated_at": "2016-06-14T15:02:57.012Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 26,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 26,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:26:01.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:26:01.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
+ "new_path": "files/ruby/feature.rb",
+ "old_path": "files/ruby/feature.rb",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 26,
+ "created_at": "2016-06-14T15:02:36.421Z",
+ "updated_at": "2016-06-14T15:02:36.474Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "1"
+ },
+ "events": [
+ {
+ "id": 222,
+ "target_type": "MergeRequest",
+ "target_id": 26,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:36.496Z",
+ "updated_at": "2016-06-14T15:02:36.496Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 186,
+ "target_type": "MergeRequest",
+ "target_id": 26,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:36.496Z",
+ "updated_at": "2016-06-14T15:02:36.496Z",
+ "action": 1,
+ "author_id": 1
+ }
+ ]
+ },
+ {
+ "id": 15,
+ "target_branch": "test-7",
+ "source_branch": "test-1",
+ "source_project_id": 5,
+ "author_id": 22,
+ "assignee_id": 16,
+ "title": "Qui accusantium et inventore facilis doloribus occaecati officiis.",
+ "created_at": "2016-06-14T15:02:25.168Z",
+ "updated_at": "2016-06-14T15:02:59.521Z",
+ "milestone_id": 17,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 7,
+ "description": "Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 777,
+ "note": "Pariatur voluptas placeat aspernatur culpa suscipit soluta.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:59.348Z",
+ "updated_at": "2016-06-14T15:02:59.348Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 778,
+ "note": "Alias et iure mollitia suscipit molestiae voluptatum nostrum asperiores.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:59.372Z",
+ "updated_at": "2016-06-14T15:02:59.372Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 779,
+ "note": "Laudantium qui eum qui sunt.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:59.395Z",
+ "updated_at": "2016-06-14T15:02:59.395Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 780,
+ "note": "Quas rem est iusto ut delectus fugiat recusandae mollitia.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:59.418Z",
+ "updated_at": "2016-06-14T15:02:59.418Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 781,
+ "note": "Repellendus ab et qui nesciunt.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:59.444Z",
+ "updated_at": "2016-06-14T15:02:59.444Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 782,
+ "note": "Non possimus voluptatum odio qui ut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:59.469Z",
+ "updated_at": "2016-06-14T15:02:59.469Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 783,
+ "note": "Dolores repellendus eum ducimus quam ab dolorem quia.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:59.494Z",
+ "updated_at": "2016-06-14T15:02:59.494Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 784,
+ "note": "Facilis dolorem aut corrupti id ratione occaecati.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:59.520Z",
+ "updated_at": "2016-06-14T15:02:59.520Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 15,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T13:22:56.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T13:22:56.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 15,
+ "created_at": "2016-06-14T15:02:25.171Z",
+ "updated_at": "2016-06-14T15:02:25.230Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ },
+ "events": [
+ {
+ "id": 223,
+ "target_type": "MergeRequest",
+ "target_id": 15,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:25.262Z",
+ "updated_at": "2016-06-14T15:02:25.262Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 175,
+ "target_type": "MergeRequest",
+ "target_id": 15,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:25.262Z",
+ "updated_at": "2016-06-14T15:02:25.262Z",
+ "action": 1,
+ "author_id": 22
+ }
+ ]
+ },
+ {
+ "id": 14,
+ "target_branch": "fix",
+ "source_branch": "test-3",
+ "source_project_id": 5,
+ "author_id": 20,
+ "assignee_id": 20,
+ "title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.",
+ "created_at": "2016-06-14T15:02:24.760Z",
+ "updated_at": "2016-06-14T15:02:59.749Z",
+ "milestone_id": 20,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 6,
+ "description": "Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 785,
+ "note": "Atque cupiditate necessitatibus deserunt minus natus odit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:59.559Z",
+ "updated_at": "2016-06-14T15:02:59.559Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 786,
+ "note": "Non dolorem provident mollitia nesciunt optio ex eveniet.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:59.587Z",
+ "updated_at": "2016-06-14T15:02:59.587Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 787,
+ "note": "Similique officia nemo quasi commodi accusantium quae qui.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:59.621Z",
+ "updated_at": "2016-06-14T15:02:59.621Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 788,
+ "note": "Et est et alias ad dolor qui.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:59.650Z",
+ "updated_at": "2016-06-14T15:02:59.650Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 789,
+ "note": "Numquam temporibus ratione voluptatibus aliquid.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:59.675Z",
+ "updated_at": "2016-06-14T15:02:59.675Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 790,
+ "note": "Ut ex aliquam consectetur perferendis est hic aut quia.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:59.703Z",
+ "updated_at": "2016-06-14T15:02:59.703Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 791,
+ "note": "Esse eos quam quaerat aut ut asperiores officiis.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:59.726Z",
+ "updated_at": "2016-06-14T15:02:59.726Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 792,
+ "note": "Sint facilis accusantium iure blanditiis.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:59.748Z",
+ "updated_at": "2016-06-14T15:02:59.748Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 14,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "ddd4ff416a931589c695eb4f5b23f844426f6928",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T14:14:43.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T14:14:43.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ },
+ {
+ "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "048721d90c449b244b7b4c53a9186b04330174ec"
+ ],
+ "authored_date": "2015-12-07T12:52:12.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "marin@gitlab.com",
+ "committed_date": "2015-12-07T12:52:12.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "marin@gitlab.com"
+ },
+ {
+ "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "message": "LFS object pointer.\n",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849"
+ ],
+ "authored_date": "2015-12-07T11:54:28.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "maxlazio@gmail.com",
+ "committed_date": "2015-12-07T11:54:28.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "maxlazio@gmail.com"
+ },
+ {
+ "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
+ "parent_ids": [
+ "d2d430676773caa88cdaf7c55944073b2fd5561a"
+ ],
+ "authored_date": "2015-11-13T16:27:12.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T16:27:12.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
+ ],
+ "authored_date": "2015-11-13T08:50:17.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:50:17.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "message": "Add GitLab SVG\n",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
+ ],
+ "authored_date": "2015-11-13T08:39:43.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:39:43.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
+ "parent_ids": [
+ "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "66eceea0db202bb39c4e445e8ca28689645366c5"
+ ],
+ "authored_date": "2015-11-13T07:21:40.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T07:21:40.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "message": "add spaces in whitespace file\n",
+ "parent_ids": [
+ "08f22f255f082689c0d7d39d19205085311542bc"
+ ],
+ "authored_date": "2015-11-13T06:01:27.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:01:27.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "parent_ids": [
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T06:00:16.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:00:16.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T05:23:14.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T05:23:14.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "message": "add whitespace in empty\n",
+ "parent_ids": [
+ "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
+ ],
+ "authored_date": "2015-11-13T05:08:45.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:45.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "message": "add empty file\n",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
+ ],
+ "authored_date": "2015-11-13T05:08:04.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:04.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "message": "Add ISO-8859 test file\n",
+ "parent_ids": [
+ "e56497bb5f03a90a51293fc6d516788730953899"
+ ],
+ "authored_date": "2015-08-25T17:53:12.000+02:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@packetzoom.com",
+ "committed_date": "2015-08-25T17:53:12.000+02:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@packetzoom.com"
+ },
+ {
+ "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
+ ],
+ "authored_date": "2015-01-10T22:23:29.000+01:00",
+ "author_name": "Sytse Sijbrandij",
+ "author_email": "sytse@gitlab.com",
+ "committed_date": "2015-01-10T22:23:29.000+01:00",
+ "committer_name": "Sytse Sijbrandij",
+ "committer_email": "sytse@gitlab.com"
+ },
+ {
+ "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "message": "add directory structure for tree_helper spec\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2015-01-10T21:28:18.000+01:00",
+ "author_name": "marmis85",
+ "author_email": "marmis85@gmail.com",
+ "committed_date": "2015-01-10T21:28:18.000+01:00",
+ "committer_name": "marmis85",
+ "committer_email": "marmis85@gmail.com"
+ },
+ {
+ "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ ],
+ "authored_date": "2014-02-27T10:01:38.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ ],
+ "authored_date": "2014-02-27T09:57:31.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "d14d6c0abdd253381df51a723d58691b2ee1ab08"
+ ],
+ "authored_date": "2014-02-27T09:54:21.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:54:21.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
+ ],
+ "authored_date": "2014-02-27T09:49:50.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:49:50.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:48:32.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:48:32.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "new_path": ".DS_Store",
+ "old_path": ".DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "new_path": ".gitignore",
+ "old_path": ".gitignore",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "new_path": ".gitmodules",
+ "old_path": ".gitmodules",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "new_path": "CHANGELOG",
+ "old_path": "CHANGELOG",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "new_path": "encoding/iso8859.txt",
+ "old_path": "encoding/iso8859.txt",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "new_path": "files/.DS_Store",
+ "old_path": "files/.DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "new_path": "files/images/wm.svg",
+ "old_path": "files/images/wm.svg",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "new_path": "files/lfs/lfs_object.iso",
+ "old_path": "files/lfs/lfs_object.iso",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "new_path": "files/ruby/popen.rb",
+ "old_path": "files/ruby/popen.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "new_path": "files/ruby/regex.rb",
+ "old_path": "files/ruby/regex.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "new_path": "files/whitespace",
+ "old_path": "files/whitespace",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "new_path": "foo/bar/.gitkeep",
+ "old_path": "foo/bar/.gitkeep",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "new_path": "gitlab-grack",
+ "old_path": "gitlab-grack",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "new_path": "gitlab-shell",
+ "old_path": "gitlab-shell",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 14,
+ "created_at": "2016-06-14T15:02:24.770Z",
+ "updated_at": "2016-06-14T15:02:25.007Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "15"
+ },
+ "events": [
+ {
+ "id": 224,
+ "target_type": "MergeRequest",
+ "target_id": 14,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:25.113Z",
+ "updated_at": "2016-06-14T15:02:25.113Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 174,
+ "target_type": "MergeRequest",
+ "target_id": 14,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:25.113Z",
+ "updated_at": "2016-06-14T15:02:25.113Z",
+ "action": 1,
+ "author_id": 20
+ }
+ ]
+ },
+ {
+ "id": 13,
+ "target_branch": "improve/awesome",
+ "source_branch": "test-8",
+ "source_project_id": 5,
+ "author_id": 16,
+ "assignee_id": 25,
+ "title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.",
+ "created_at": "2016-06-14T15:02:24.415Z",
+ "updated_at": "2016-06-14T15:02:59.958Z",
+ "milestone_id": 17,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 5,
+ "description": "Est eaque quasi qui qui. Similique voluptatem impedit iusto ratione reprehenderit. Itaque est illum ut nulla aut.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 793,
+ "note": "In illum maxime aperiam nulla est aspernatur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:02:59.782Z",
+ "updated_at": "2016-06-14T15:02:59.782Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 794,
+ "note": "Enim quia perferendis cum distinctio tenetur optio voluptas veniam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:02:59.807Z",
+ "updated_at": "2016-06-14T15:02:59.807Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 795,
+ "note": "Dolor ad quia quis pariatur ducimus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:02:59.831Z",
+ "updated_at": "2016-06-14T15:02:59.831Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 796,
+ "note": "Et a odio voluptate aut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:02:59.854Z",
+ "updated_at": "2016-06-14T15:02:59.854Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 797,
+ "note": "Quis nihil temporibus voluptatum modi minima a ut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:02:59.879Z",
+ "updated_at": "2016-06-14T15:02:59.879Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 798,
+ "note": "Ut alias consequatur in nostrum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:02:59.904Z",
+ "updated_at": "2016-06-14T15:02:59.904Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 799,
+ "note": "Voluptatibus aperiam assumenda et neque sint libero.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:02:59.926Z",
+ "updated_at": "2016-06-14T15:02:59.926Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 800,
+ "note": "Veritatis voluptatem dolor dolores magni quo ut ipsa fuga.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:02:59.956Z",
+ "updated_at": "2016-06-14T15:02:59.956Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 13,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "0bfedc29d30280c7e8564e19f654584b459e5868",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:25:23.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T15:25:23.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ },
+ {
+ "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "048721d90c449b244b7b4c53a9186b04330174ec"
+ ],
+ "authored_date": "2015-12-07T12:52:12.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "marin@gitlab.com",
+ "committed_date": "2015-12-07T12:52:12.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "marin@gitlab.com"
+ },
+ {
+ "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "message": "LFS object pointer.\n",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849"
+ ],
+ "authored_date": "2015-12-07T11:54:28.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "maxlazio@gmail.com",
+ "committed_date": "2015-12-07T11:54:28.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "maxlazio@gmail.com"
+ },
+ {
+ "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
+ "parent_ids": [
+ "d2d430676773caa88cdaf7c55944073b2fd5561a"
+ ],
+ "authored_date": "2015-11-13T16:27:12.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T16:27:12.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
+ ],
+ "authored_date": "2015-11-13T08:50:17.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:50:17.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "message": "Add GitLab SVG\n",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
+ ],
+ "authored_date": "2015-11-13T08:39:43.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:39:43.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
+ "parent_ids": [
+ "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "66eceea0db202bb39c4e445e8ca28689645366c5"
+ ],
+ "authored_date": "2015-11-13T07:21:40.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T07:21:40.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "message": "add spaces in whitespace file\n",
+ "parent_ids": [
+ "08f22f255f082689c0d7d39d19205085311542bc"
+ ],
+ "authored_date": "2015-11-13T06:01:27.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:01:27.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "parent_ids": [
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T06:00:16.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:00:16.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T05:23:14.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T05:23:14.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "message": "add whitespace in empty\n",
+ "parent_ids": [
+ "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
+ ],
+ "authored_date": "2015-11-13T05:08:45.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:45.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "message": "add empty file\n",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
+ ],
+ "authored_date": "2015-11-13T05:08:04.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:04.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "message": "Add ISO-8859 test file\n",
+ "parent_ids": [
+ "e56497bb5f03a90a51293fc6d516788730953899"
+ ],
+ "authored_date": "2015-08-25T17:53:12.000+02:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@packetzoom.com",
+ "committed_date": "2015-08-25T17:53:12.000+02:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@packetzoom.com"
+ },
+ {
+ "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
+ ],
+ "authored_date": "2015-01-10T22:23:29.000+01:00",
+ "author_name": "Sytse Sijbrandij",
+ "author_email": "sytse@gitlab.com",
+ "committed_date": "2015-01-10T22:23:29.000+01:00",
+ "committer_name": "Sytse Sijbrandij",
+ "committer_email": "sytse@gitlab.com"
+ },
+ {
+ "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "message": "add directory structure for tree_helper spec\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2015-01-10T21:28:18.000+01:00",
+ "author_name": "marmis85",
+ "author_email": "marmis85@gmail.com",
+ "committed_date": "2015-01-10T21:28:18.000+01:00",
+ "committer_name": "marmis85",
+ "committer_email": "marmis85@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "new_path": "CHANGELOG",
+ "old_path": "CHANGELOG",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "new_path": "encoding/iso8859.txt",
+ "old_path": "encoding/iso8859.txt",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "new_path": "files/images/wm.svg",
+ "old_path": "files/images/wm.svg",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "new_path": "files/lfs/lfs_object.iso",
+ "old_path": "files/lfs/lfs_object.iso",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "new_path": "files/whitespace",
+ "old_path": "files/whitespace",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "new_path": "foo/bar/.gitkeep",
+ "old_path": "foo/bar/.gitkeep",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 13,
+ "created_at": "2016-06-14T15:02:24.420Z",
+ "updated_at": "2016-06-14T15:02:24.561Z",
+ "base_commit_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "real_size": "7"
+ },
+ "events": [
+ {
+ "id": 225,
+ "target_type": "MergeRequest",
+ "target_id": 13,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:24.636Z",
+ "updated_at": "2016-06-14T15:02:24.636Z",
+ "action": 1,
+ "author_id": 16
+ },
+ {
+ "id": 173,
+ "target_type": "MergeRequest",
+ "target_id": 13,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:24.636Z",
+ "updated_at": "2016-06-14T15:02:24.636Z",
+ "action": 1,
+ "author_id": 16
+ }
+ ]
+ },
+ {
+ "id": 12,
+ "target_branch": "flatten-dirs",
+ "source_branch": "test-2",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": 22,
+ "title": "In a rerum harum nihil accusamus aut quia nobis non.",
+ "created_at": "2016-06-14T15:02:24.000Z",
+ "updated_at": "2016-06-14T15:03:00.225Z",
+ "milestone_id": 19,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 4,
+ "description": "Nam magnam odit velit rerum. Sapiente dolore sunt saepe debitis. Culpa maiores ut ad dolores dolorem et.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 801,
+ "note": "Nihil dicta molestias expedita atque.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:03:00.001Z",
+ "updated_at": "2016-06-14T15:03:00.001Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 802,
+ "note": "Illum culpa voluptas enim accusantium deserunt.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:03:00.034Z",
+ "updated_at": "2016-06-14T15:03:00.034Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 803,
+ "note": "Dicta esse aliquam laboriosam unde alias.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:03:00.065Z",
+ "updated_at": "2016-06-14T15:03:00.065Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 804,
+ "note": "Dicta autem et sed molestiae ut quae.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:03:00.097Z",
+ "updated_at": "2016-06-14T15:03:00.097Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 805,
+ "note": "Ut ut temporibus voluptas dolore quia velit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:03:00.129Z",
+ "updated_at": "2016-06-14T15:03:00.129Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 806,
+ "note": "Dolores similique sint pariatur error id quia fugit aut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:03:00.162Z",
+ "updated_at": "2016-06-14T15:03:00.162Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 807,
+ "note": "Quisquam provident nihil aperiam voluptatem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:03:00.193Z",
+ "updated_at": "2016-06-14T15:03:00.193Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 808,
+ "note": "Similique quo vero expedita deserunt ipsam earum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:03:00.224Z",
+ "updated_at": "2016-06-14T15:03:00.224Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 12,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T14:08:21.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T14:08:21.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ },
+ {
+ "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "048721d90c449b244b7b4c53a9186b04330174ec"
+ ],
+ "authored_date": "2015-12-07T12:52:12.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "marin@gitlab.com",
+ "committed_date": "2015-12-07T12:52:12.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "marin@gitlab.com"
+ },
+ {
+ "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "message": "LFS object pointer.\n",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849"
+ ],
+ "authored_date": "2015-12-07T11:54:28.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "maxlazio@gmail.com",
+ "committed_date": "2015-12-07T11:54:28.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "maxlazio@gmail.com"
+ },
+ {
+ "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
+ "parent_ids": [
+ "d2d430676773caa88cdaf7c55944073b2fd5561a"
+ ],
+ "authored_date": "2015-11-13T16:27:12.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T16:27:12.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
+ ],
+ "authored_date": "2015-11-13T08:50:17.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:50:17.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "message": "Add GitLab SVG\n",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
+ ],
+ "authored_date": "2015-11-13T08:39:43.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:39:43.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
+ "parent_ids": [
+ "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "66eceea0db202bb39c4e445e8ca28689645366c5"
+ ],
+ "authored_date": "2015-11-13T07:21:40.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T07:21:40.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "message": "add spaces in whitespace file\n",
+ "parent_ids": [
+ "08f22f255f082689c0d7d39d19205085311542bc"
+ ],
+ "authored_date": "2015-11-13T06:01:27.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:01:27.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "parent_ids": [
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T06:00:16.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:00:16.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T05:23:14.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T05:23:14.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "message": "add whitespace in empty\n",
+ "parent_ids": [
+ "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
+ ],
+ "authored_date": "2015-11-13T05:08:45.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:45.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "message": "add empty file\n",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
+ ],
+ "authored_date": "2015-11-13T05:08:04.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:04.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "message": "Add ISO-8859 test file\n",
+ "parent_ids": [
+ "e56497bb5f03a90a51293fc6d516788730953899"
+ ],
+ "authored_date": "2015-08-25T17:53:12.000+02:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@packetzoom.com",
+ "committed_date": "2015-08-25T17:53:12.000+02:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@packetzoom.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "new_path": "CHANGELOG",
+ "old_path": "CHANGELOG",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "new_path": "encoding/iso8859.txt",
+ "old_path": "encoding/iso8859.txt",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "new_path": "files/images/wm.svg",
+ "old_path": "files/images/wm.svg",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "new_path": "files/lfs/lfs_object.iso",
+ "old_path": "files/lfs/lfs_object.iso",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "new_path": "files/whitespace",
+ "old_path": "files/whitespace",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 12,
+ "created_at": "2016-06-14T15:02:24.006Z",
+ "updated_at": "2016-06-14T15:02:24.169Z",
+ "base_commit_sha": "e56497bb5f03a90a51293fc6d516788730953899",
+ "real_size": "6"
+ },
+ "events": [
+ {
+ "id": 226,
+ "target_type": "MergeRequest",
+ "target_id": 12,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:24.253Z",
+ "updated_at": "2016-06-14T15:02:24.253Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 172,
+ "target_type": "MergeRequest",
+ "target_id": 12,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:24.253Z",
+ "updated_at": "2016-06-14T15:02:24.253Z",
+ "action": 1,
+ "author_id": 1
+ }
+ ]
+ },
+ {
+ "id": 11,
+ "target_branch": "test-15",
+ "source_branch": "'test'",
+ "source_project_id": 5,
+ "author_id": 16,
+ "assignee_id": 16,
+ "title": "Corporis provident similique perspiciatis dolores eos animi.",
+ "created_at": "2016-06-14T15:02:23.767Z",
+ "updated_at": "2016-06-14T15:03:00.475Z",
+ "milestone_id": 18,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 3,
+ "description": "Libero nesciunt mollitia quis odit eos vero quasi. Iure voluptatem ut sint pariatur voluptates ut aut. Laborum possimus unde illum ipsum eum.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 809,
+ "note": "Omnis ratione laboriosam dolores qui.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:03:00.260Z",
+ "updated_at": "2016-06-14T15:03:00.260Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 810,
+ "note": "Voluptas voluptates pariatur dolores maxime est voluptas.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:03:00.290Z",
+ "updated_at": "2016-06-14T15:03:00.290Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 811,
+ "note": "Sit perspiciatis facilis ipsum consequatur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:03:00.323Z",
+ "updated_at": "2016-06-14T15:03:00.323Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 812,
+ "note": "Ut neque aliquam nam et est.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:03:00.349Z",
+ "updated_at": "2016-06-14T15:03:00.349Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 813,
+ "note": "Et debitis rerum minima sit aut dolorem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:03:00.374Z",
+ "updated_at": "2016-06-14T15:03:00.374Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 814,
+ "note": "Ea nisi earum fugit iste aperiam consequatur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:03:00.397Z",
+ "updated_at": "2016-06-14T15:03:00.397Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 815,
+ "note": "Amet ratione consequatur laudantium rerum voluptas est nobis.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:03:00.450Z",
+ "updated_at": "2016-06-14T15:03:00.450Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 816,
+ "note": "Ab ducimus cumque quia dolorem vitae sint beatae rerum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:03:00.474Z",
+ "updated_at": "2016-06-14T15:03:00.474Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 11,
+ "state": "empty",
+ "st_commits": null,
+ "st_diffs": [
+
+ ],
+ "merge_request_id": 11,
+ "created_at": "2016-06-14T15:02:23.772Z",
+ "updated_at": "2016-06-14T15:02:23.833Z",
+ "base_commit_sha": "e56497bb5f03a90a51293fc6d516788730953899",
+ "real_size": null
+ },
+ "events": [
+ {
+ "id": 227,
+ "target_type": "MergeRequest",
+ "target_id": 11,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:23.865Z",
+ "updated_at": "2016-06-14T15:02:23.865Z",
+ "action": 1,
+ "author_id": 16
+ },
+ {
+ "id": 171,
+ "target_type": "MergeRequest",
+ "target_id": 11,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:23.865Z",
+ "updated_at": "2016-06-14T15:02:23.865Z",
+ "action": 1,
+ "author_id": 16
+ }
+ ]
+ },
+ {
+ "id": 10,
+ "target_branch": "feature",
+ "source_branch": "test-5",
+ "source_project_id": 5,
+ "author_id": 20,
+ "assignee_id": 25,
+ "title": "Eligendi reprehenderit doloribus quia et sit id.",
+ "created_at": "2016-06-14T15:02:23.014Z",
+ "updated_at": "2016-06-14T15:03:00.685Z",
+ "milestone_id": 20,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 2,
+ "description": "Ut dolor quia aliquid dolore et nisi. Est minus suscipit enim quaerat sapiente consequatur rerum. Eveniet provident consequatur dolor accusantium reiciendis.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 817,
+ "note": "Recusandae et voluptas enim qui et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:03:00.510Z",
+ "updated_at": "2016-06-14T15:03:00.510Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 818,
+ "note": "Asperiores dolorem rerum ipsum totam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:03:00.538Z",
+ "updated_at": "2016-06-14T15:03:00.538Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 819,
+ "note": "Qui quam et iure quasi provident cumque itaque sequi.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:03:00.562Z",
+ "updated_at": "2016-06-14T15:03:00.562Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 820,
+ "note": "Sint accusantium aliquid iste qui iusto minus vel.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:03:00.585Z",
+ "updated_at": "2016-06-14T15:03:00.585Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 821,
+ "note": "Dolor corrupti dolorem blanditiis voluptas.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:03:00.610Z",
+ "updated_at": "2016-06-14T15:03:00.610Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 822,
+ "note": "Est perferendis assumenda aliquam aliquid sit ipsum ullam aut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:03:00.635Z",
+ "updated_at": "2016-06-14T15:03:00.635Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 823,
+ "note": "Hic neque reiciendis quaerat maiores.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:03:00.659Z",
+ "updated_at": "2016-06-14T15:03:00.659Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 824,
+ "note": "Sequi architecto doloribus ut vel autem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:03:00.683Z",
+ "updated_at": "2016-06-14T15:03:00.683Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 10,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T14:43:23.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T14:43:23.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ },
+ {
+ "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "048721d90c449b244b7b4c53a9186b04330174ec"
+ ],
+ "authored_date": "2015-12-07T12:52:12.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "marin@gitlab.com",
+ "committed_date": "2015-12-07T12:52:12.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "marin@gitlab.com"
+ },
+ {
+ "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "message": "LFS object pointer.\n",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849"
+ ],
+ "authored_date": "2015-12-07T11:54:28.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "maxlazio@gmail.com",
+ "committed_date": "2015-12-07T11:54:28.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "maxlazio@gmail.com"
+ },
+ {
+ "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
+ "parent_ids": [
+ "d2d430676773caa88cdaf7c55944073b2fd5561a"
+ ],
+ "authored_date": "2015-11-13T16:27:12.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T16:27:12.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
+ ],
+ "authored_date": "2015-11-13T08:50:17.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:50:17.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "message": "Add GitLab SVG\n",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
+ ],
+ "authored_date": "2015-11-13T08:39:43.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:39:43.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
+ "parent_ids": [
+ "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "66eceea0db202bb39c4e445e8ca28689645366c5"
+ ],
+ "authored_date": "2015-11-13T07:21:40.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T07:21:40.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "message": "add spaces in whitespace file\n",
+ "parent_ids": [
+ "08f22f255f082689c0d7d39d19205085311542bc"
+ ],
+ "authored_date": "2015-11-13T06:01:27.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:01:27.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "parent_ids": [
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T06:00:16.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:00:16.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T05:23:14.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T05:23:14.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "message": "add whitespace in empty\n",
+ "parent_ids": [
+ "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
+ ],
+ "authored_date": "2015-11-13T05:08:45.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:45.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "message": "add empty file\n",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
+ ],
+ "authored_date": "2015-11-13T05:08:04.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:04.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "message": "Add ISO-8859 test file\n",
+ "parent_ids": [
+ "e56497bb5f03a90a51293fc6d516788730953899"
+ ],
+ "authored_date": "2015-08-25T17:53:12.000+02:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@packetzoom.com",
+ "committed_date": "2015-08-25T17:53:12.000+02:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@packetzoom.com"
+ },
+ {
+ "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
+ ],
+ "authored_date": "2015-01-10T22:23:29.000+01:00",
+ "author_name": "Sytse Sijbrandij",
+ "author_email": "sytse@gitlab.com",
+ "committed_date": "2015-01-10T22:23:29.000+01:00",
+ "committer_name": "Sytse Sijbrandij",
+ "committer_email": "sytse@gitlab.com"
+ },
+ {
+ "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "message": "add directory structure for tree_helper spec\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2015-01-10T21:28:18.000+01:00",
+ "author_name": "marmis85",
+ "author_email": "marmis85@gmail.com",
+ "committed_date": "2015-01-10T21:28:18.000+01:00",
+ "committer_name": "marmis85",
+ "committer_email": "marmis85@gmail.com"
+ },
+ {
+ "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ ],
+ "authored_date": "2014-02-27T10:01:38.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ ],
+ "authored_date": "2014-02-27T09:57:31.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "d14d6c0abdd253381df51a723d58691b2ee1ab08"
+ ],
+ "authored_date": "2014-02-27T09:54:21.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:54:21.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
+ ],
+ "authored_date": "2014-02-27T09:49:50.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:49:50.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:48:32.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:48:32.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "new_path": ".DS_Store",
+ "old_path": ".DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "new_path": ".gitignore",
+ "old_path": ".gitignore",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "new_path": ".gitmodules",
+ "old_path": ".gitmodules",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "new_path": "CHANGELOG",
+ "old_path": "CHANGELOG",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "new_path": "encoding/iso8859.txt",
+ "old_path": "encoding/iso8859.txt",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "new_path": "files/.DS_Store",
+ "old_path": "files/.DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "new_path": "files/images/wm.svg",
+ "old_path": "files/images/wm.svg",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "new_path": "files/lfs/lfs_object.iso",
+ "old_path": "files/lfs/lfs_object.iso",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "new_path": "files/ruby/popen.rb",
+ "old_path": "files/ruby/popen.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "new_path": "files/ruby/regex.rb",
+ "old_path": "files/ruby/regex.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "new_path": "files/whitespace",
+ "old_path": "files/whitespace",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "new_path": "foo/bar/.gitkeep",
+ "old_path": "foo/bar/.gitkeep",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "new_path": "gitlab-grack",
+ "old_path": "gitlab-grack",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "new_path": "gitlab-shell",
+ "old_path": "gitlab-shell",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 10,
+ "created_at": "2016-06-14T15:02:23.019Z",
+ "updated_at": "2016-06-14T15:02:23.493Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "15"
+ },
+ "events": [
+ {
+ "id": 228,
+ "target_type": "MergeRequest",
+ "target_id": 10,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:23.660Z",
+ "updated_at": "2016-06-14T15:02:23.660Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 170,
+ "target_type": "MergeRequest",
+ "target_id": 10,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:23.660Z",
+ "updated_at": "2016-06-14T15:02:23.660Z",
+ "action": 1,
+ "author_id": 20
+ }
+ ]
+ },
+ {
+ "id": 9,
+ "target_branch": "test-6",
+ "source_branch": "test-12",
+ "source_project_id": 5,
+ "author_id": 16,
+ "assignee_id": 6,
+ "title": "Et ipsam voluptas velit sequi illum ut.",
+ "created_at": "2016-06-14T15:02:22.825Z",
+ "updated_at": "2016-06-14T15:03:00.904Z",
+ "milestone_id": 16,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 1,
+ "description": "Eveniet nihil ratione veniam similique qui aut sapiente tempora. Sed praesentium iusto dignissimos possimus id repudiandae quo nihil. Qui doloremque autem et iure fugit.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+ "force_remove_source_branch": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 825,
+ "note": "Aliquid voluptatem consequatur voluptas ex perspiciatis.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-06-14T15:03:00.722Z",
+ "updated_at": "2016-06-14T15:03:00.722Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 826,
+ "note": "Itaque optio voluptatem praesentium voluptas.",
+ "noteable_type": "MergeRequest",
+ "author_id": 25,
+ "created_at": "2016-06-14T15:03:00.745Z",
+ "updated_at": "2016-06-14T15:03:00.745Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 3"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 827,
+ "note": "Ut est corporis fuga asperiores delectus excepturi aperiam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-06-14T15:03:00.771Z",
+ "updated_at": "2016-06-14T15:03:00.771Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 828,
+ "note": "Similique ea dolore officiis temporibus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 20,
+ "created_at": "2016-06-14T15:03:00.798Z",
+ "updated_at": "2016-06-14T15:03:00.798Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ottis Schuster II"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 829,
+ "note": "Qui laudantium qui quae quis.",
+ "noteable_type": "MergeRequest",
+ "author_id": 16,
+ "created_at": "2016-06-14T15:03:00.828Z",
+ "updated_at": "2016-06-14T15:03:00.828Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Rhett Emmerich IV"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 830,
+ "note": "Et vel voluptas amet laborum qui soluta.",
+ "noteable_type": "MergeRequest",
+ "author_id": 15,
+ "created_at": "2016-06-14T15:03:00.850Z",
+ "updated_at": "2016-06-14T15:03:00.850Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Burdette Bernier"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 831,
+ "note": "Enim ad consequuntur assumenda provident voluptatem similique deleniti.",
+ "noteable_type": "MergeRequest",
+ "author_id": 6,
+ "created_at": "2016-06-14T15:03:00.876Z",
+ "updated_at": "2016-06-14T15:03:00.876Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Ari Wintheiser"
+ },
+ "events": [
+
+ ]
+ },
+ {
+ "id": 832,
+ "note": "Officiis sequi commodi pariatur totam fugiat voluptas corporis dignissimos.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-06-14T15:03:00.902Z",
+ "updated_at": "2016-06-14T15:03:00.902Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+
+ ]
+ }
+ ],
+ "merge_request_diff": {
+ "id": 9,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:44:02.000+01:00",
+ "author_name": "James Lopez",
+ "author_email": "james@jameslopez.es",
+ "committed_date": "2016-01-19T15:44:02.000+01:00",
+ "committer_name": "James Lopez",
+ "committer_email": "james@jameslopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 9,
+ "created_at": "2016-06-14T15:02:22.829Z",
+ "updated_at": "2016-06-14T15:02:22.900Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ },
+ "events": [
+ {
+ "id": 229,
+ "target_type": "MergeRequest",
+ "target_id": 9,
+ "title": null,
+ "data": null,
+ "project_id": 36,
+ "created_at": "2016-06-14T15:02:22.927Z",
+ "updated_at": "2016-06-14T15:02:22.927Z",
+ "action": 1,
+ "author_id": 16
+ },
+ {
+ "id": 169,
+ "target_type": "MergeRequest",
+ "target_id": 9,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:22.927Z",
+ "updated_at": "2016-06-14T15:02:22.927Z",
+ "action": 1,
+ "author_id": 16
+ }
+ ]
+ }
+ ],
+ "pipelines": [
+ {
+ "id": 36,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.755Z",
+ "updated_at": "2016-03-22T15:20:35.755Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "notes": [
+ {
+ "id": 999,
+ "note": "Natus rerum qui dolorem dolorum voluptas.",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:19:59.469Z",
+ "updated_at": "2016-03-22T15:19:59.469Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ }
+ ],
+ "statuses": [
+ {
+ "id": 71,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.630Z",
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.772Z",
+ "updated_at": "2016-03-29T06:28:12.634Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 72,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.",
+ "created_at": "2016-03-22T15:20:35.777Z",
+ "updated_at": "2016-03-22T15:20:35.777Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.757Z",
+ "updated_at": "2016-03-22T15:20:35.757Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 74,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.",
+ "created_at": "2016-03-22T15:20:35.846Z",
+ "updated_at": "2016-03-22T15:20:35.846Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 73,
+ "project_id": 5,
+ "status": "canceled",
+ "finished_at": null,
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.842Z",
+ "updated_at": "2016-03-22T15:20:35.842Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 38,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.759Z",
+ "updated_at": "2016-03-22T15:20:35.759Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 76,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.",
+ "created_at": "2016-03-22T15:20:35.882Z",
+ "updated_at": "2016-03-22T15:20:35.882Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 75,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.",
+ "created_at": "2016-03-22T15:20:35.864Z",
+ "updated_at": "2016-03-22T15:20:35.864Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.761Z",
+ "updated_at": "2016-03-22T15:20:35.761Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 78,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.",
+ "created_at": "2016-03-22T15:20:35.927Z",
+ "updated_at": "2016-03-22T15:20:35.927Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 77,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.",
+ "created_at": "2016-03-22T15:20:35.905Z",
+ "updated_at": "2016-03-22T15:20:35.905Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 40,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.763Z",
+ "updated_at": "2016-03-22T15:20:35.763Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 79,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.695Z",
+ "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.",
+ "created_at": "2016-03-22T15:20:35.950Z",
+ "updated_at": "2016-03-29T06:28:12.696Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 80,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.",
+ "created_at": "2016-03-22T15:20:35.966Z",
+ "updated_at": "2016-03-22T15:20:35.966Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ }
+ ],
+ "variables": [
+
+ ],
+ "triggers": [
+
+ ],
+ "deploy_keys": [
+
+ ],
+ "services": [
+ {
+ "id": 164,
+ "title": null,
+ "project_id": 5,
+ "created_at": "2016-06-14T15:02:07.372Z",
+ "updated_at": "2016-06-14T15:02:07.372Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "issue_tracker",
+ "default": true,
+ "wiki_page_events": true
+ },
+ {
+ "id": 100,
+ "title": "JetBrains TeamCity CI",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.315Z",
+ "updated_at": "2016-06-14T15:01:51.315Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 99,
+ "title": "Slack",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.303Z",
+ "updated_at": "2016-06-14T15:01:51.303Z",
+ "active": false,
+ "properties": {
+ "notify_only_broken_builds": true
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 98,
+ "title": "Redmine",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.289Z",
+ "updated_at": "2016-06-14T15:01:51.289Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "issue_tracker",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 97,
+ "title": "Pushover",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.277Z",
+ "updated_at": "2016-06-14T15:01:51.277Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 96,
+ "title": "PivotalTracker",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.267Z",
+ "updated_at": "2016-06-14T15:01:51.267Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 95,
+ "title": "JIRA",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.255Z",
+ "updated_at": "2016-06-14T15:01:51.255Z",
+ "active": false,
+ "properties": {
+ "api_url": "",
+ "jira_issue_transition_id": "2"
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "issue_tracker",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 94,
+ "title": "Irker (IRC gateway)",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.232Z",
+ "updated_at": "2016-06-14T15:01:51.232Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 93,
+ "title": "HipChat",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.219Z",
+ "updated_at": "2016-06-14T15:01:51.219Z",
+ "active": false,
+ "properties": {
+ "notify_only_broken_builds": true
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 92,
+ "title": "Gemnasium",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.202Z",
+ "updated_at": "2016-06-14T15:01:51.202Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 91,
+ "title": "Flowdock",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.182Z",
+ "updated_at": "2016-06-14T15:01:51.182Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 90,
+ "title": "External Wiki",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.166Z",
+ "updated_at": "2016-06-14T15:01:51.166Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 89,
+ "title": "Emails on push",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.153Z",
+ "updated_at": "2016-06-14T15:01:51.153Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 88,
+ "title": "Drone CI",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.139Z",
+ "updated_at": "2016-06-14T15:01:51.139Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 87,
+ "title": "Custom Issue Tracker",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.125Z",
+ "updated_at": "2016-06-14T15:01:51.125Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "issue_tracker",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 86,
+ "title": "Campfire",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.113Z",
+ "updated_at": "2016-06-14T15:01:51.113Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 85,
+ "title": "Builds emails",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.090Z",
+ "updated_at": "2016-06-14T15:01:51.090Z",
+ "active": false,
+ "properties": {
+ "notify_only_broken_builds": true
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 84,
+ "title": "Buildkite",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.080Z",
+ "updated_at": "2016-06-14T15:01:51.080Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 83,
+ "title": "Atlassian Bamboo CI",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.067Z",
+ "updated_at": "2016-06-14T15:01:51.067Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "ci",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 82,
+ "title": "Assembla",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.047Z",
+ "updated_at": "2016-06-14T15:01:51.047Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
+ "id": 81,
+ "title": "Asana",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.031Z",
+ "updated_at": "2016-06-14T15:01:51.031Z",
+ "active": false,
+ "properties": {
+
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "build_events": true,
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ }
+ ],
+ "hooks": [
+
+ ],
+ "protected_branches": [
+
+ ]
+} \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
new file mode 100644
index 00000000000..a72aaa44e82
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
+ describe 'restore project tree' do
+ let(:user) { create(:user) }
+ let(:namespace) { create(:namespace, owner: user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
+ let(:project) { create(:empty_project, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+ let(:restored_project_json) { project_tree_restorer.restore }
+
+ before do
+ allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ end
+
+ context 'JSON' do
+ it 'restores models based on JSON' do
+ expect(restored_project_json).to be true
+ end
+
+ it 'creates a valid pipeline note' do
+ restored_project_json
+
+ expect(Ci::Pipeline.first.notes).not_to be_empty
+ end
+
+ it 'restores the correct event' do
+ restored_project_json
+
+ expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
new file mode 100644
index 00000000000..1424de9e60b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
+ describe 'saves the project tree into a json object' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:project_tree_saver) { described_class.new(project: project, shared: shared) }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:user) { create(:user) }
+ let(:project) { setup_project }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves project successfully' do
+ expect(project_tree_saver.save).to be true
+ end
+
+ context 'JSON' do
+ let(:saved_project_json) do
+ project_tree_saver.save
+ project_json(project_tree_saver.full_path)
+ end
+
+ it 'saves the correct json' do
+ expect(saved_project_json).to include({ "visibility_level" => 20 })
+ end
+
+ it 'has events' do
+ expect(saved_project_json['milestones'].first['events']).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(saved_project_json['milestones']).not_to be_empty
+ end
+
+ it 'has merge requests' do
+ expect(saved_project_json['merge_requests']).not_to be_empty
+ end
+
+ it 'has labels' do
+ expect(saved_project_json['labels']).not_to be_empty
+ end
+
+ it 'has snippets' do
+ expect(saved_project_json['snippets']).not_to be_empty
+ end
+
+ it 'has snippet notes' do
+ expect(saved_project_json['snippets'].first['notes']).not_to be_empty
+ end
+
+ it 'has releases' do
+ expect(saved_project_json['releases']).not_to be_empty
+ end
+
+ it 'has issues' do
+ expect(saved_project_json['issues']).not_to be_empty
+ end
+
+ it 'has issue comments' do
+ expect(saved_project_json['issues'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on issue comments' do
+ expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has project members' do
+ expect(saved_project_json['project_members']).not_to be_empty
+ end
+
+ it 'has merge requests diffs' do
+ expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ it 'has merge requests comments' do
+ expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on merge requests comments' do
+ expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has pipeline statuses' do
+ expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty
+ end
+
+ it 'has pipeline builds' do
+ expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
+ end
+
+ it 'has pipeline commits' do
+ expect(saved_project_json['pipelines']).not_to be_empty
+ end
+
+ it 'has ci pipeline notes' do
+ expect(saved_project_json['pipelines'].first['notes']).not_to be_empty
+ end
+ end
+ end
+
+ def setup_project
+ issue = create(:issue, assignee: user)
+ merge_request = create(:merge_request)
+ label = create(:label)
+ snippet = create(:project_snippet)
+ release = create(:release)
+
+ project = create(:project,
+ :public,
+ issues: [issue],
+ merge_requests: [merge_request],
+ labels: [label],
+ snippets: [snippet],
+ releases: [release]
+ )
+
+ commit_status = create(:commit_status, project: project)
+
+ ci_pipeline = create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
+
+ create(:ci_build, pipeline: ci_pipeline, project: project)
+ milestone = create(:milestone, project: project)
+ create(:note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_pipeline.sha)
+
+ create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+
+ project
+ end
+
+ def project_json(filename)
+ JSON.parse(IO.read(filename))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
new file mode 100644
index 00000000000..b76e14deca1
--- /dev/null
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Reader, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+ let(:test_config) { 'spec/support/import_export/import_export.yml' }
+ let(:project_tree_hash) do
+ {
+ only: [:name, :path],
+ include: [:issues, :labels,
+ { merge_requests: {
+ only: [:id],
+ except: [:iid],
+ include: [:merge_request_diff, :merge_request_test]
+ } },
+ { commit_statuses: { include: :commit } }]
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
+ end
+
+ it 'generates hash from project tree config' do
+ expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash)
+ end
+
+ context 'individual scenarios' do
+ it 'generates the correct hash for a single project relation' do
+ setup_yaml(project_tree: [:issues])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
+ end
+
+ it 'generates the correct hash for a multiple project relation' do
+ setup_yaml(project_tree: [:issues, :snippets])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets])
+ end
+
+ it 'generates the correct hash for a single sub-relation' do
+ setup_yaml(project_tree: [issues: [:notes]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }])
+ end
+
+ it 'generates the correct hash for a multiple sub-relation' do
+ setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }])
+ end
+
+ it 'generates the correct hash for a sub-relation with another sub-relation' do
+ setup_yaml(project_tree: [merge_requests: [notes: :author]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }])
+ end
+
+ it 'generates the correct hash for a relation with included attributes' do
+ setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }])
+ end
+
+ it 'generates the correct hash for a relation with excluded attributes' do
+ setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }])
+ end
+
+ it 'generates the correct hash for a relation with both excluded and included attributes' do
+ setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }])
+ end
+
+ it 'generates the correct hash for a relation with custom methods' do
+ setup_yaml(project_tree: [:issues], methods: { issues: [:name] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
+ end
+
+ def setup_yaml(hash)
+ allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
new file mode 100644
index 00000000000..135e99bc953
--- /dev/null
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RepoSaver, services: true do
+ describe 'bundle a project Git repo' do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:bundler) { described_class.new(project: project, shared: shared) }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'bundles the repo successfully' do
+ expect(bundler.save).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
new file mode 100644
index 00000000000..b628da0f3e8
--- /dev/null
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::WikiRepoSaver, services: true do
+ describe 'bundle a wiki Git repo' do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
+ let!(:project_wiki) { ProjectWiki.new(project, user) }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ project_wiki.wiki
+ project_wiki.create_page("index", "test content")
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'bundles the repo successfully' do
+ expect(wiki_bundler.save).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb
deleted file mode 100644
index f758cb8693c..00000000000
--- a/spec/lib/gitlab/import_url_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::ImportUrl do
-
- let(:credentials) { { user: 'blah', password: 'password' } }
- let(:import_url) do
- Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials)
- end
-
- describe :full_url do
- it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") }
- end
-
- describe :sanitized_url do
- it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") }
- end
-
- describe :credentials do
- it { expect(import_url.credentials).to eq(credentials) }
- end
-end
diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb
new file mode 100644
index 00000000000..b5ca89dd242
--- /dev/null
+++ b/spec/lib/gitlab/lazy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Lazy, lib: true do
+ let(:dummy) { double(:dummy) }
+
+ context 'when not calling any methods' do
+ it 'does not call the supplied block' do
+ expect(dummy).not_to receive(:foo)
+
+ described_class.new { dummy.foo }
+ end
+ end
+
+ context 'when calling a method on the object' do
+ it 'lazy loads the value returned by the block' do
+ expect(dummy).to receive(:foo).and_return('foo')
+
+ lazy = described_class.new { dummy.foo }
+
+ expect(lazy.to_s).to eq('foo')
+ end
+ end
+
+ describe '#respond_to?' do
+ it 'returns true for a method defined on the wrapped object' do
+ lazy = described_class.new { 'foo' }
+
+ expect(lazy).to respond_to(:downcase)
+ end
+
+ it 'returns false for a method not defined on the wrapped object' do
+ lazy = described_class.new { 'foo' }
+
+ expect(lazy).not_to respond_to(:quack)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 6a53ed1db64..69c49051156 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -32,7 +32,6 @@ describe Gitlab::LDAP::AuthHash, lib: true do
end
context "without overridden attributes" do
-
it "has the correct username" do
expect(auth_hash.username).to eq("123456")
end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
index 5852b31ab3a..659facd6c19 100644
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -17,17 +17,20 @@ describe Gitlab::Lfs::Router, lib: true do
}
end
- let(:lfs_router_auth) { new_lfs_router(project, user) }
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
- let(:lfs_router_public_auth) { new_lfs_router(public_project, user) }
- let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) }
- let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) }
- let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) }
+ let(:lfs_router_auth) { new_lfs_router(project, user: user) }
+ let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
+ let(:lfs_router_noauth) { new_lfs_router(project) }
+ let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
+ let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
+ let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
+ let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
+ let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
+ let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
let(:sample_size) { 499013 }
- let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
- let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
+ let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
+ let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
describe 'when lfs is disabled' do
before do
@@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do
context 'with required headers' do
before do
+ project.lfs_objects << lfs_object
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
end
@@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do
context 'when user has project access' do
before do
- project.lfs_objects << lfs_object
project.team << [user, :master]
end
@@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
end
end
+
+ context 'when CI is authorized' do
+ it "responds with status 200" do
+ expect(lfs_router_ci_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
end
context 'without required headers' do
@@ -134,143 +148,145 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'download' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- describe 'when user has download access' do
+ shared_examples 'an authorized requests' do
+ context 'when downloading an lfs object that is assigned to our project' do
before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :reporter]
+ project.lfs_objects << lfs_object
end
- context 'when downloading an lfs object that is assigned to our project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and href to download' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
+ end
- context 'when downloading an lfs object that is assigned to other project' do
- before do
- public_project.lfs_objects << lfs_object
- end
+ context 'when downloading an lfs object that is assigned to other project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
- it 'responds with status 200 and error message' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and error message' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading a lfs object that does not exist' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ context 'when downloading a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- it "responds with status 200 and error message" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 and error message" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading one new and one existing lfs object' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
+ context 'when downloading one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ project.lfs_objects << lfs_object
+ end
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
end
+ end
+
+ context 'when user is authenticated' do
+ let(:auth) { authorize(user) }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ project.team << [user, role]
+ end
+
+ it_behaves_like 'an authorized requests' do
+ let(:role) { :reporter }
+ let(:router) { lfs_router_auth }
+ end
context 'when user does is not member of the project' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -278,11 +294,7 @@ describe Gitlab::Lfs::Router, lib: true do
end
context 'when user does not have download access' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- context 'when user is not authenticated' do
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }],
+ env["HTTP_AUTHORIZATION"] = auth
+ end
- }.to_json
- env['rack.input'] = StringIO.new(body)
+ it_behaves_like 'an authorized requests' do
+ let(:router) { lfs_router_ci_auth }
end
+ end
+ context 'when user is not authenticated' do
describe 'is accessing public project' do
before do
public_project.lfs_objects << lfs_object
@@ -338,17 +351,17 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'upload' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+ describe 'when request is authenticated' do
describe 'when user has project push access' do
before do
@auth = authorize(user)
@@ -368,7 +381,7 @@ describe Gitlab::Lfs::Router, lib: true do
expect(response['objects']).to be_kind_of(Array)
expect(response['objects'].first['oid']).to eq(sample_oid)
expect(response['objects'].first['size']).to eq(sample_size)
- expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
@@ -430,7 +443,7 @@ describe Gitlab::Lfs::Router, lib: true do
expect(response_body['objects'].last['oid']).to eq(sample_oid)
expect(response_body['objects'].last['size']).to eq(sample_size)
- expect(response_body['objects'].last).to_not have_key('actions')
+ expect(response_body['objects'].last).not_to have_key('actions')
end
end
end
@@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call.first).to eq(403)
end
end
- end
- context 'when user is not authenticated' do
- before do
- env['rack.input'] = StringIO.new(
- { 'objects' => [], 'operation' => 'upload' }.to_json
- )
+ context 'when CI is authorized' do
+ it 'responds with 401' do
+ expect(lfs_router_ci_auth.try_call.first).to eq(401)
+ end
end
+ end
+ context 'when user is not authenticated' do
context 'when user has push access' do
before do
project.team << [user, :master]
@@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
end
+
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ end
+
+ it "responds with status 403" do
+ expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
+ end
+ end
end
describe 'unsupported' do
@@ -490,13 +515,68 @@ describe Gitlab::Lfs::Router, lib: true do
env['REQUEST_METHOD'] = 'PUT'
end
- describe 'to one project' do
- describe 'when user has push access to the project' do
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
before do
- project.team << [user, :master]
+ env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(router.try_call).to eq(nil)
end
+ end
+ end
+
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'to one project' do
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(project)
@@ -524,100 +604,35 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_auth }
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent with a malformed headers' do
- before do
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
- end
-
- it 'does not recognize it as a valid lfs command' do
- expect(lfs_router_noauth.try_call).to eq(nil)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_ci_auth }
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
+ it_behaves_like 'unauthorized'
+ end
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
+ context 'for unauthenticated' do
+ let(:router) { new_lfs_router(project) }
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
end
- describe "to a forked project" do
+ describe 'to a forked project' do
let(:forked_project) { fork_project(public_project, user) }
- describe 'when user has push access to the project' do
- before do
- forked_project.team << [user_two, :master]
- end
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ forked_project.team << [user_two, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(forked_project)
@@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_forked_auth }
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_forked_ci_auth }
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'unauthorized'
+ end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ context 'for unauthenticated' do
+ let(:router) { lfs_router_forked_noauth }
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
describe 'and second project not related to fork or a source project' do
let(:second_project) { create(:project) }
- let(:lfs_router_second_project) { new_lfs_router(second_project, user) }
+ let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
before do
public_project.lfs_objects << lfs_object
@@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
- def new_lfs_router(project, user)
- Gitlab::Lfs::Router.new(project, user, request)
+ def new_lfs_router(project, user: nil, ci: false)
+ Gitlab::Lfs::Router.new(project, user, ci, request)
end
def header_for_upload_authorize(project)
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 5c885a7a982..8809b7e3f12 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -9,9 +9,31 @@ describe Gitlab::Metrics::Instrumentation do
text
end
+ class << self
+ def buzz(text = 'buzz')
+ text
+ end
+ private :buzz
+
+ def flaky(text = 'flaky')
+ text
+ end
+ protected :flaky
+ end
+
def bar(text = 'bar')
text
end
+
+ def wadus(text = 'wadus')
+ text
+ end
+ private :wadus
+
+ def chaf(text = 'chaf')
+ text
+ end
+ protected :chaf
end
allow(@dummy).to receive(:name).and_return('Dummy')
@@ -56,12 +78,8 @@ 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')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy.foo')
@dummy.foo
end
@@ -70,7 +88,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
- expect(transaction).to_not receive(:add_metric)
+ expect(transaction).not_to receive(:add_metric)
@dummy.foo
end
@@ -139,12 +157,8 @@ 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')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy#bar')
@dummy.new.bar
end
@@ -153,7 +167,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
- expect(transaction).to_not receive(:add_metric)
+ expect(transaction).not_to receive(:add_metric)
@dummy.new.bar
end
@@ -214,6 +228,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_methods(@dummy)
expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:foo).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected class methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:flaky).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:buzz).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -226,7 +255,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_methods(@dummy)
- expect(@dummy).to_not respond_to(:_original_kittens)
+ expect(@dummy).not_to respond_to(:_original_kittens)
end
it 'can take a block to determine if a method should be instrumented' do
@@ -234,7 +263,7 @@ describe Gitlab::Metrics::Instrumentation do
false
end
- expect(@dummy).to_not respond_to(:_original_foo)
+ expect(@dummy).not_to respond_to(:_original_foo)
end
end
@@ -247,6 +276,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:bar).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:chaf).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:wadus).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -259,7 +303,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
- expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
+ expect(@dummy.new.method(:kittens).source_location.first).not_to match(/instrumentation\.rb/)
end
it 'can take a block to determine if a method should be instrumented' do
@@ -267,7 +311,7 @@ describe Gitlab::Metrics::Instrumentation do
false
end
- expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ expect(@dummy.new.method(:bar).source_location.first).not_to match(/instrumentation\.rb/)
end
end
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
new file mode 100644
index 00000000000..8d05081eecb
--- /dev/null
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::MethodCall do
+ let(:method_call) { described_class.new('Foo#bar', 'foo') }
+
+ describe '#measure' do
+ it 'measures the performance of the supplied block' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.real_time).to be_a_kind_of(Numeric)
+ expect(method_call.cpu_time).to be_a_kind_of(Numeric)
+ expect(method_call.call_count).to eq(1)
+ end
+ end
+
+ describe '#to_metric' do
+ it 'returns a Metric instance' do
+ method_call.measure { 'foo' }
+ metric = method_call.to_metric
+
+ expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
+ expect(metric.series).to eq('foo')
+
+ expect(metric.values[:duration]).to be_a_kind_of(Numeric)
+ expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
+ expect(metric.values[:call_count]).to an_instance_of(Fixnum)
+
+ expect(metric.tags).to eq({ method: 'Foo#bar' })
+ end
+ end
+
+ describe '#above_threshold?' do
+ it 'returns false when the total call time is not above the threshold' do
+ expect(method_call.above_threshold?).to eq(false)
+ end
+
+ it 'returns true when the total call time is above the threshold' do
+ expect(method_call).to receive(:real_time).and_return(9000)
+
+ expect(method_call.above_threshold?).to eq(true)
+ end
+ end
+
+ describe '#call_count' do
+ context 'without any method calls' do
+ it 'returns 0' do
+ expect(method_call.call_count).to eq(0)
+ end
+ end
+
+ context 'with method calls' do
+ it 'returns the number of method calls' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.call_count).to eq(1)
+ end
+ end
+ end
+
+ describe '#cpu_time' do
+ context 'without timings' do
+ it 'returns 0.0' do
+ expect(method_call.cpu_time).to eq(0.0)
+ end
+ end
+
+ context 'with timings' do
+ it 'returns the total CPU time' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.cpu_time >= 0.0).to be(true)
+ end
+ end
+ end
+
+ describe '#real_time' do
+ context 'without timings' do
+ it 'returns 0.0' do
+ expect(method_call.real_time).to eq(0.0)
+ end
+ end
+
+ context 'with timings' do
+ it 'returns the total real time' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.real_time >= 0.0).to be(true)
+ 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 b99be4e1060..f264ed64029 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -31,6 +31,20 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ allow(app).to receive(:call).with(env)
+
+ expect(middleware).to receive(:tag_endpoint).
+ with(an_instance_of(Gitlab::Metrics::Transaction), env)
+
+ middleware.call(env)
+ end
end
describe '#transaction_from_env' do
@@ -44,6 +58,22 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.values[:request_method]).to eq('GET')
expect(transaction.values[:request_uri]).to eq('/foo')
end
+
+ context "when URI includes sensitive parameters" do
+ let(:env) do
+ {
+ 'REQUEST_METHOD' => 'GET',
+ 'REQUEST_URI' => '/foo?private_token=my-token',
+ 'PATH_INFO' => '/foo',
+ 'QUERY_STRING' => 'private_token=my_token',
+ 'action_dispatch.parameter_filter' => [:private_token]
+ }
+ end
+
+ it 'stores the request URI with the sensitive parameters filtered' do
+ expect(transaction.values[:request_uri]).to eq('/foo?private_token=[FILTERED]')
+ end
+ end
end
describe '#tag_controller' do
@@ -60,4 +90,19 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.action).to eq('TestController#show')
end
end
+
+ describe '#tag_endpoint' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'tags a transaction with the method and path of the route in the grape endpount' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ middleware.tag_endpoint(transaction, env)
+
+ expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
index 38da77adc9f..1ab923b58cf 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -72,14 +72,25 @@ describe Gitlab::Metrics::Sampler do
end
end
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric).
- with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
- at_least(:once).
- and_call_original
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
+ at_least(:once).
+ and_call_original
+
+ sampler.sample_objects
+ end
- sampler.sample_objects
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler).not_to receive(:add_metric).
+ with('object_counts', an_instance_of(Hash), type: nil)
+
+ sampler.sample_objects
+ end
end
end
@@ -130,7 +141,7 @@ describe Gitlab::Metrics::Sampler do
100.times do
interval = sampler.sleep_interval
- expect(interval).to_not eq(last)
+ expect(interval).not_to eq(last)
last = interval
end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index e520a968999..4d2aa03e722 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::Metrics::SidekiqMiddleware do
let(:middleware) { described_class.new }
+ let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } }
describe '#call' do
it 'tracks the transaction' do
@@ -11,9 +12,23 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
- middleware.call(worker, 'test', :test) { nil }
+ middleware.call(worker, message, :test) { nil }
+ end
+
+ it 'tracks the transaction (for messages without `enqueued_at`)' do
+ 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(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+
+ middleware.call(worker, {}, :test) { nil }
end
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index e3293a01207..49699ffe28f 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
describe 'without a current transaction' do
it 'simply returns' do
expect_any_instance_of(Gitlab::Metrics::Transaction).
- to_not receive(:increment)
+ not_to receive(:increment)
subscriber.sql(event)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index e01b0b4bd21..d986c6fac43 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -9,16 +9,71 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_read' do
it 'increments the cache_read duration' do
expect(subscriber).to receive(:increment).
- with(:cache_read_duration, event.duration)
+ with(:cache_read, event.duration)
subscriber.cache_read(event)
end
+
+ context 'with a transaction' do
+ before do
+ allow(subscriber).to receive(:current_transaction).
+ and_return(transaction)
+ end
+
+ context 'with hit event' do
+ let(:event) { double(:event, duration: 15.2, payload: { hit: true }) }
+
+ it 'increments the cache_read_hit count' do
+ expect(transaction).to receive(:increment).
+ with(:cache_read_hit_count, 1)
+ expect(transaction).to receive(:increment).
+ with(any_args).at_least(1) # Other calls
+
+ subscriber.cache_read(event)
+ end
+
+ context 'when super operation is fetch' do
+ let(:event) { double(:event, duration: 15.2, payload: { hit: true, super_operation: :fetch }) }
+
+ it 'does not increment cache read miss' do
+ expect(transaction).not_to receive(:increment).
+ with(:cache_read_hit_count, 1)
+
+ subscriber.cache_read(event)
+ end
+ end
+ end
+
+ context 'with miss event' do
+ let(:event) { double(:event, duration: 15.2, payload: { hit: false }) }
+
+ it 'increments the cache_read_miss count' do
+ expect(transaction).to receive(:increment).
+ with(:cache_read_miss_count, 1)
+ expect(transaction).to receive(:increment).
+ with(any_args).at_least(1) # Other calls
+
+ subscriber.cache_read(event)
+ end
+
+ context 'when super operation is fetch' do
+ let(:event) { double(:event, duration: 15.2, payload: { hit: false, super_operation: :fetch }) }
+
+ it 'does not increment cache read miss' do
+ expect(transaction).not_to receive(:increment).
+ with(:cache_read_miss_count, 1)
+
+ subscriber.cache_read(event)
+ end
+ end
+ end
+ end
end
describe '#cache_write' do
it 'increments the cache_write duration' do
expect(subscriber).to receive(:increment).
- with(:cache_write_duration, event.duration)
+ with(:cache_write, event.duration)
subscriber.cache_write(event)
end
@@ -27,7 +82,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_delete' do
it 'increments the cache_delete duration' do
expect(subscriber).to receive(:increment).
- with(:cache_delete_duration, event.duration)
+ with(:cache_delete, event.duration)
subscriber.cache_delete(event)
end
@@ -36,12 +91,60 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_exist?' do
it 'increments the cache_exists duration' do
expect(subscriber).to receive(:increment).
- with(:cache_exists_duration, event.duration)
+ with(:cache_exists, event.duration)
subscriber.cache_exist?(event)
end
end
+ describe '#cache_fetch_hit' do
+ context 'without a transaction' do
+ it 'returns' do
+ expect(transaction).not_to receive(:increment)
+
+ subscriber.cache_fetch_hit(event)
+ end
+ end
+
+ context 'with a transaction' do
+ before do
+ allow(subscriber).to receive(:current_transaction).
+ and_return(transaction)
+ end
+
+ it 'increments the cache_read_hit count' do
+ expect(transaction).to receive(:increment).
+ with(:cache_read_hit_count, 1)
+
+ subscriber.cache_fetch_hit(event)
+ end
+ end
+ end
+
+ describe '#cache_generate' do
+ context 'without a transaction' do
+ it 'returns' do
+ expect(transaction).not_to receive(:increment)
+
+ subscriber.cache_generate(event)
+ end
+ end
+
+ context 'with a transaction' do
+ before do
+ allow(subscriber).to receive(:current_transaction).
+ and_return(transaction)
+ end
+
+ it 'increments the cache_fetch_miss count' do
+ expect(transaction).to receive(:increment).
+ with(:cache_read_miss_count, 1)
+
+ subscriber.cache_generate(event)
+ end
+ end
+ end
+
describe '#increment' do
context 'without a transaction' do
it 'returns' do
@@ -62,9 +165,15 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
with(:cache_duration, event.duration)
expect(transaction).to receive(:increment).
+ with(:cache_count, 1)
+
+ expect(transaction).to receive(:increment).
with(:cache_delete_duration, event.duration)
- subscriber.increment(:cache_delete_duration, event.duration)
+ expect(transaction).to receive(:increment).
+ with(:cache_delete_count, 1)
+
+ subscriber.increment(:cache_delete, event.duration)
end
end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index d6ae54e25e8..cf0e282c2fb 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -28,8 +28,20 @@ describe Gitlab::Metrics::System do
end
describe '.cpu_time' do
- it 'returns a Fixnum' do
- expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
+ it 'returns a Float' do
+ expect(described_class.cpu_time).to be_an_instance_of(Float)
+ end
+ end
+
+ describe '.real_time' do
+ it 'returns a Float' do
+ expect(described_class.real_time).to be_an_instance_of(Float)
+ end
+ end
+
+ describe '.monotonic_time' do
+ it 'returns a Float' do
+ expect(described_class.monotonic_time).to be_an_instance_of(Float)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 1d5a51a157e..3b1c67a2147 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -46,6 +46,22 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#measure_method' do
+ it 'adds a new method if it does not exist already' do
+ transaction.measure_method('Foo#bar') { 'foo' }
+
+ expect(transaction.methods['Foo#bar']).
+ to be_an_instance_of(Gitlab::Metrics::MethodCall)
+ end
+
+ it 'adds timings to an existing method call' do
+ transaction.measure_method('Foo#bar') { 'foo' }
+ transaction.measure_method('Foo#bar') { 'foo' }
+
+ expect(transaction.methods['Foo#bar'].call_count).to eq(2)
+ end
+ end
+
describe '#increment' do
it 'increments a counter' do
transaction.increment(:time, 1)
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
new file mode 100644
index 00000000000..fd6f684db0c
--- /dev/null
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::RailsQueueDuration do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:env) { {} }
+ let(:transaction) { double(:transaction) }
+
+ before { expect(app).to receive(:call).with(env).and_return('yay') }
+
+ describe '#call' do
+ it 'calls the app when metrics are disabled' do
+ expect(Gitlab::Metrics).to receive(:current_transaction).and_return(nil)
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ context 'when metrics are enabled' do
+ before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) }
+
+ it 'calls the app when metrics are enabled but no timing header is found' do
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ it 'sets proxy_flight_time and calls the app when the header is present' do
+ env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+ expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
+ expect(middleware.call(env)).to eq('yay')
+ 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 f093d0a0d8b..3d6bcdfd873 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/note_data_builder_spec.rb
@@ -9,7 +9,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
before(:each) do
expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url)
- expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note))
+ expect(data[:object_attributes][:url])
+ .to eq(Gitlab::UrlBuilder.build(note))
expect(data[:object_kind]).to eq('note')
expect(data[:user]).to eq(user.hook_attrs)
end
@@ -26,7 +27,7 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on commit diff' do
- let(:note) { create(:note_on_commit_diff, project: project) }
+ let(:note) { create(:diff_note_on_commit, project: project) }
it 'returns the note and commit-specific data' do
expect(data).to have_key(:commit)
@@ -37,13 +38,21 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
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, project: project) }
+ let(:issue) do
+ create(:issue, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let(:note) do
+ create(:note_on_issue, noteable: issue, project: project)
+ end
it 'returns the note and issue-specific data' do
expect(data).to have_key(:issue)
- 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']
+ expect(data[:issue].except('updated_at'))
+ .to eq(issue.reload.hook_attrs.except('updated_at'))
+ expect(data[:issue]['updated_at'])
+ .to be > issue.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -51,13 +60,23 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
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, project: project) }
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time,
+ updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project)
+ end
it 'returns the note and merge request data' do
expect(data).to have_key(:merge_request)
- 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']
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.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'
@@ -65,13 +84,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
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, project: project) }
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time, updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:diff_note_on_merge_request, noteable: merge_request,
+ project: project)
+ end
it 'returns the note and merge request diff data' do
expect(data).to have_key(:merge_request)
- 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']
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.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'
@@ -79,13 +107,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
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, project: project) }
+ let!(:snippet) do
+ create(:project_snippet, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let!(:note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project)
+ end
it 'returns the note and project snippet data' do
expect(data).to have_key(:snippet)
- 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']
+ expect(data[:snippet].except('updated_at'))
+ .to eq(snippet.reload.hook_attrs.except('updated_at'))
+ expect(data[:snippet]['updated_at'])
+ .to be > snippet.hook_attrs['updated_at']
end
include_examples 'project hook data'
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 6727a83e58a..1fca8a13037 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -51,12 +51,25 @@ describe Gitlab::OAuth::User, lib: true do
end
context 'provider was external, now has been removed' do
- it 'should mark existing user internal' do
+ it 'should not mark external user as internal' do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
oauth_user.save
expect(gl_user).to be_valid
- expect(gl_user.external).to be_falsey
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'provider is not external' do
+ context 'when adding a new OAuth identity' do
+ it 'should not promote an external user to internal' do
+ user = create(:user, email: 'john@mail.com', external: true)
+ user.identities.create(provider: provider, extern_uid: uid)
+
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
end
end
@@ -122,13 +135,12 @@ describe Gitlab::OAuth::User, lib: true 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(: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 omniauth identities" do
oauth_user.save
@@ -169,7 +181,6 @@ describe Gitlab::OAuth::User, lib: true do
end
end
end
-
end
describe 'blocking' do
@@ -203,7 +214,7 @@ describe Gitlab::OAuth::User, lib: true 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(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(oauth_user).to receive(:ldap_person).and_return(ldap_user)
end
@@ -255,7 +266,6 @@ describe Gitlab::OAuth::User, lib: true do
end
end
-
context 'sign-in' do
before do
oauth_user.save
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 795cf241278..e8b236426e9 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -10,7 +10,7 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
context 'zero status' do
before do
- @output, @status = @klass.new.popen(%W(ls), path)
+ @output, @status = @klass.new.popen(%w(ls), path)
end
it { expect(@status).to be_zero }
@@ -19,7 +19,7 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
context 'non-zero status' do
before do
- @output, @status = @klass.new.popen(%W(cat NOTHING), path)
+ @output, @status = @klass.new.popen(%w(cat NOTHING), path)
end
it { expect(@status).to eq(1) }
@@ -34,7 +34,7 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
context 'without a directory argument' do
before do
- @output, @status = @klass.new.popen(%W(ls))
+ @output, @status = @klass.new.popen(%w(ls))
end
it { expect(@status).to be_zero }
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index db0ff95b4f5..270b89972d7 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -43,6 +43,18 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ results = described_class.new(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')
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 7fc34139eff..6bd7393aaa7 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -4,7 +4,6 @@ describe Gitlab::PushDataBuilder, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
-
describe '.build_sample' do
let(:data) { described_class.build_sample(project, user) }
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7c617723e6d..7b4ccc83915 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -105,7 +105,8 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
- expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project)]
end
end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index c2a51d9249c..56bf08e7041 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do
allow(ldap_user).to receive(:email) { %w(john@mail.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)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
@@ -163,7 +164,14 @@ describe Gitlab::Saml::User, lib: true do
end
context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ before do
+ create(:omniauth_user,
+ email: 'john@mail.com',
+ extern_uid: 'uid=user1,ou=People,dc=example',
+ provider: 'ldapmain',
+ username: 'john')
+ end
+
it 'adds the omniauth identity to the LDAP account' do
saml_user.save
@@ -176,11 +184,36 @@ describe Gitlab::Saml::User, lib: true do
{ provider: 'saml', extern_uid: uid }
])
end
+
+ it 'saves successfully on subsequent tries, when both identities are present' do
+ saml_user.save
+ local_saml_user = described_class.new(auth_hash)
+ local_saml_user.save
+
+ expect(local_saml_user.gl_user).to be_valid
+ expect(local_saml_user.gl_user).to be_persisted
+ end
+ end
+
+ context 'user has SAML user, and wants to add their LDAP identity' do
+ it 'adds the LDAP identity to the existing SAML user' do
+ create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john')
+ local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash)
+ local_saml_user = described_class.new(local_hash)
+ local_saml_user.save
+ local_gl_user = local_saml_user.gl_user
+
+ expect(local_gl_user).to be_valid
+ expect(local_gl_user.identities.length).to eql 2
+ identities_as_hash = local_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=user1,ou=People,dc=example' }
+ ])
+ end
end
end
end
end
-
end
describe 'blocking' do
diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb
new file mode 100644
index 00000000000..030c2063ab2
--- /dev/null
+++ b/spec/lib/gitlab/sanitizers/svg_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Sanitizers::SVG do
+ let(:scrubber) { Gitlab::Sanitizers::SVG::Scrubber.new }
+ let(:namespace) { double(Nokogiri::XML::Namespace, prefix: 'xlink', href: 'http://www.w3.org/1999/xlink') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: '#awesome_id') }
+
+ describe '.clean' do
+ let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
+ let(:data) { open(input_svg_path).read }
+ let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
+ let(:sanitized) { open(sanitized_svg_path).read }
+
+ it 'delegates sanitization to scrubber' do
+ expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once)
+ described_class.clean(data)
+ end
+
+ it 'returns sanitized data' do
+ expect(described_class.clean(data)).to eq(sanitized)
+ end
+ end
+
+ context 'scrubber' do
+ describe '#scrub' do
+ let(:invalid_element) { double(Nokogiri::XML::Node, name: 'invalid', value: 'invalid') }
+ let(:invalid_attribute) { double(Nokogiri::XML::Attr, name: 'invalid', namespace: nil) }
+ let(:valid_element) { double(Nokogiri::XML::Node, name: 'use') }
+
+ it 'removes an invalid element' do
+ expect(invalid_element).to receive(:unlink)
+
+ scrubber.scrub(invalid_element)
+ end
+
+ it 'removes an invalid attribute' do
+ allow(valid_element).to receive(:attribute_nodes) { [invalid_attribute] }
+ expect(invalid_attribute).to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid element' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(valid_element).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid namespaced attributes' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(namespaced_attr).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+ end
+
+ describe '#attribute_name_with_namespace' do
+ it 'returns name with prefix when attribute is namespaced' do
+ expect(scrubber.attribute_name_with_namespace(namespaced_attr)).to eq('xlink:href')
+ end
+ end
+
+ describe '#unsafe_href?' do
+ let(:unsafe_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: 'http://evilsite.example.com/random.svg') }
+
+ it 'returns true if href attribute is an external url' do
+ expect(scrubber.unsafe_href?(unsafe_attr)).to be_truthy
+ end
+
+ it 'returns false if href atttribute is an internal reference' do
+ expect(scrubber.unsafe_href?(namespaced_attr)).to be_falsey
+ end
+ end
+
+ describe '#data_attribute?' do
+ let(:data_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: nil, value: 'gitlab is awesome') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: namespace, value: 'gitlab is awesome') }
+ let(:other_attr) { double(Nokogiri::XML::Attr, name: 'something', namespace: nil, value: 'content') }
+
+ it 'returns true if is a valid data attribute' do
+ expect(scrubber.data_attribute?(data_attr)).to be_truthy
+ end
+
+ it 'returns false if attribute is namespaced' do
+ expect(scrubber.data_attribute?(namespaced_attr)).to be_falsey
+ end
+
+ it 'returns false if not a data attribute' do
+ expect(scrubber.data_attribute?(other_attr)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index f4afe597e8d..1bb444bf34f 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
+ it 'should not list confidential issues for project members with guest role' do
+ project_1.team << [member, :guest]
+ project_2.team << [member, :guest]
+
+ results = described_class.new(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')
diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb
index de6bb86c5dd..2ae79b50e77 100644
--- a/spec/lib/gitlab/sherlock/collection_spec.rb
+++ b/spec/lib/gitlab/sherlock/collection_spec.rb
@@ -11,13 +11,13 @@ describe Gitlab::Sherlock::Collection, lib: true do
it 'adds a new transaction' do
collection.add(transaction)
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
it 'is aliased as <<' do
collection << transaction
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
end
@@ -47,7 +47,7 @@ describe Gitlab::Sherlock::Collection, lib: true do
it 'returns false for a collection with a transaction' do
collection.add(transaction)
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
end
diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb
index 05da915ccfd..0a620428138 100644
--- a/spec/lib/gitlab/sherlock/query_spec.rb
+++ b/spec/lib/gitlab/sherlock/query_spec.rb
@@ -85,7 +85,7 @@ FROM users;
frames = query.application_backtrace
expect(frames).to be_an_instance_of(Array)
- expect(frames).to_not be_empty
+ expect(frames).not_to be_empty
frames.each do |frame|
expect(frame.path).to start_with(Rails.root.to_s)
diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb
index 7553f2a045f..9fe18f253f0 100644
--- a/spec/lib/gitlab/sherlock/transaction_spec.rb
+++ b/spec/lib/gitlab/sherlock/transaction_spec.rb
@@ -203,7 +203,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do
end
it 'only tracks queries triggered from the transaction thread' do
- expect(transaction).to_not receive(:track_query)
+ expect(transaction).not_to receive(:track_query)
Thread.new { subscription.publish('test', time, time, nil, query_data) }.
join
@@ -226,7 +226,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do
end
it 'only tracks views rendered from the transaction thread' do
- expect(transaction).to_not receive(:track_view)
+ expect(transaction).not_to receive(:track_view)
Thread.new { subscription.publish('test', time, time, nil, view_data) }.
join
diff --git a/spec/lib/gitlab/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb
new file mode 100644
index 00000000000..bc0ec9325cc
--- /dev/null
+++ b/spec/lib/gitlab/template/gitignore_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Template::Gitignore do
+ subject { described_class }
+
+ describe '.all' do
+ it 'strips the gitignore suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitignore')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Vim')
+ expect(all).to include('Ruby')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect(subject.find('mepmep-yadida')).to be nil
+ end
+
+ it 'returns the Gitignore object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Template::Gitignore
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('*.gem')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index bf11472407a..a826b24419a 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -43,9 +43,9 @@ describe Gitlab::UrlBuilder, lib: true do
end
end
- context 'on a CommitDiff' do
+ context 'on a Commit Diff' do
it 'returns a proper URL' do
- note = build_stubbed(:note_on_commit_diff)
+ note = build_stubbed(:diff_note_on_commit)
url = described_class.build(note)
@@ -75,10 +75,10 @@ describe Gitlab::UrlBuilder, lib: true do
end
end
- context 'on a MergeRequestDiff' do
+ context 'on a MergeRequest Diff' do
it 'returns a proper URL' do
merge_request = create(:merge_request, iid: 42)
- note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request)
+ note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
url = described_class.build(note)
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
new file mode 100644
index 00000000000..59024d3290b
--- /dev/null
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::UrlSanitizer, lib: true do
+ let(:credentials) { { user: 'blah', password: 'password' } }
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: credentials)
+ end
+
+ describe '.sanitize' do
+ def sanitize_url(url)
+ # We want to try with multi-line content because is how error messages are formatted
+ described_class.sanitize(%Q{
+ remote: Not Found
+ fatal: repository '#{url}' not found
+ })
+ end
+
+ it 'mask the credentials from HTTP URLs' do
+ filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/')
+
+ expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/")
+ end
+
+ it 'mask the credentials from HTTPS URLs' do
+ filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/')
+
+ expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/")
+ end
+
+ it 'mask credentials from SSH URLs' do
+ filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git')
+
+ expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git")
+ end
+
+ it 'does not modify Git URLs' do
+ # git protocol does not support authentication
+ filtered_content = sanitize_url('git://host.test/path/to/repo.git')
+
+ expect(filtered_content).to include("git://host.test/path/to/repo.git")
+ end
+
+ it 'does not modify scp-like URLs' do
+ filtered_content = sanitize_url('user@server:project.git')
+
+ expect(filtered_content).to include("user@server:project.git")
+ end
+ end
+
+ describe '#sanitized_url' do
+ it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
+ end
+
+ describe '#credentials' do
+ it { expect(url_sanitizer.credentials).to eq(credentials) }
+ end
+
+ describe '#full_url' do
+ it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") }
+
+ it 'supports scp-like URLs' do
+ sanitizer = described_class.new('user@server:project.git')
+
+ expect(sanitizer.full_url).to eq('user@server:project.git')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d940bf05061..c5c1402e8fc 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Workhorse, lib: true do
end
it "raises an error" do
- expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError)
+ expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
end
end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c59dfea5c55..c4c107c9eea 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -8,6 +8,12 @@ describe Gitlab, lib: true do
expect(described_class.com?).to eq true
end
+ it 'is true when on staging' do
+ stub_config_setting(url: 'https://staging.gitlab.com')
+
+ expect(described_class.com?).to eq true
+ end
+
it 'is false when not on GitLab.com' do
stub_config_setting(url: 'http://example.com')
diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb
new file mode 100644
index 00000000000..18726754517
--- /dev/null
+++ b/spec/lib/json_web_token/rsa_token_spec.rb
@@ -0,0 +1,43 @@
+describe JSONWebToken::RSAToken do
+ let(:rsa_key) do
+ OpenSSL::PKey::RSA.new <<-eos.strip_heredoc
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak
+ OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ
+ iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw
+ 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu
+ H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al
+ A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l
+ 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4
+ -----END RSA PRIVATE KEY-----
+ eos
+ end
+ let(:rsa_token) { described_class.new(nil) }
+ let(:rsa_encoded) { rsa_token.encoded }
+
+ before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) }
+
+ context 'token' do
+ context 'for valid key to be validated' do
+ before { rsa_token['key'] = 'value' }
+
+ subject { JWT.decode(rsa_encoded, rsa_key) }
+
+ it { expect{subject}.not_to raise_error }
+ it { expect(subject.first).to include('key' => 'value') }
+ it do
+ expect(subject.second).to eq(
+ "typ" => "JWT",
+ "alg" => "RS256",
+ "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2")
+ end
+ end
+
+ context 'for invalid key to raise an exception' do
+ let(:new_key) { OpenSSL::PKey::RSA.generate(512) }
+ subject { JWT.decode(rsa_encoded, new_key) }
+
+ it { expect{subject}.to raise_error(JWT::DecodeError) }
+ end
+ end
+end
diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb
new file mode 100644
index 00000000000..3d955e4d774
--- /dev/null
+++ b/spec/lib/json_web_token/token_spec.rb
@@ -0,0 +1,18 @@
+describe JSONWebToken::Token do
+ let(:token) { described_class.new }
+
+ context 'custom parameters' do
+ let(:value) { 'value' }
+ before { token[:key] = value }
+
+ it { expect(token[:key]).to eq(value) }
+ it { expect(token.payload).to include(key: value) }
+ end
+
+ context 'embeds default payload' do
+ subject { token.payload }
+ let(:default) { token.send(:default_payload) }
+
+ it { is_expected.to include(default) }
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 495c5cbac00..0a9b10bebea 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -51,7 +51,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -230,7 +230,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -400,26 +400,169 @@ describe Notify do
end
end
+ describe 'project access requested' do
+ context 'for a project in a user namespace' do
+ let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.requesters.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.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 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
+ end
+
+ context 'for a project in a group' do
+ let(:group_owner) { create(:user) }
+ let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.requesters.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.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 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
+ end
+ end
+
+ describe 'project access denied' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.requesters.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('project', project.id, user.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 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ end
+ end
+
describe 'project access changed' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
- subject { Notify.project_access_granted_email(project_member.id) }
+ subject { Notify.member_access_granted_email('project', project_member.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 'has the correct subject' do
- is_expected.to have_subject /Access to project was granted/
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.human_access}/
end
+ end
- it 'contains name of project' do
- is_expected.to have_body_text /#{project.name}/
- end
+ def invite_to_project(project:, email:, inviter:)
+ ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
- it 'contains new user role' do
+ project.project_members.invite.last
+ end
+
+ describe 'project invitation' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+
+ subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
+
+ 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 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_body_text /#{project_member.invite_token}/
+ end
+ end
+
+ describe 'project invitation accepted' do
+ let(:project) { create(:project) }
+ let(:invited_user) { create(:user) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('project', project_member.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 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'project invitation declined' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.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 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
end
end
@@ -454,7 +597,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -535,27 +678,139 @@ describe Notify do
end
end
- describe 'group access changed' do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
- let(:membership) { create(:group_member, group: group, user: user) }
+ context 'for a group' do
+ describe 'group access requested' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.requesters.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('group', group_member.id) }
- subject { Notify.group_access_granted_email(membership.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_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 'contains all the useful information' do
+ is_expected.to have_subject "Request to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group_group_members_url(group)}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /Access to group was granted/
+ describe 'group access denied' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.requesters.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('group', group.id, user.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 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was denied"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ end
+ end
+
+ describe 'group access changed' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) { create(:group_member, group: group, user: user) }
+
+ subject { Notify.member_access_granted_email('group', group_member.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 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was granted"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
end
- it 'contains name of project' do
- is_expected.to have_body_text /#{group.name}/
+ def invite_to_group(group:, email:, inviter:)
+ GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+
+ group.group_members.invite.last
end
- it 'contains new user role' do
- is_expected.to have_body_text /#{membership.human_access}/
+ describe 'group invitation' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+
+ subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
+
+ 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 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ is_expected.to have_body_text /#{group_member.invite_token}/
+ end
+ end
+
+ describe 'group invitation accepted' do
+ let(:group) { create(:group) }
+ let(:invited_user) { create(:user) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('group', group_member.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 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'group invitation declined' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.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 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ end
end
end
@@ -593,7 +848,7 @@ describe Notify do
let(:user) { create(:user) }
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
+ subject { Notify.repository_push_email(project.id, 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"
@@ -606,10 +861,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /Pushed new branch master/
end
@@ -624,7 +875,7 @@ describe Notify do
let(:user) { create(:user) }
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
+ subject { Notify.repository_push_email(project.id, 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"
@@ -637,10 +888,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /Pushed new tag v1\.0/
end
@@ -654,7 +901,7 @@ describe Notify do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
+ subject { Notify.repository_push_email(project.id, 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"
@@ -667,10 +914,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /Deleted branch master/
end
@@ -680,7 +923,7 @@ describe Notify do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
+ subject { Notify.repository_push_email(project.id, 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"
@@ -693,10 +936,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /Deleted tag v1\.0/
end
@@ -709,8 +948,9 @@ describe Notify do
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false }
+ let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
- 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) }
+ subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, 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"
@@ -723,10 +963,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/
end
@@ -735,20 +971,19 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/
end
- it 'doesn not contain the misleading footer' do
+ it 'does not contain the misleading footer' do
is_expected.not_to have_body_text /you are a member of/
end
context "when set to send from committer email if domain matches" do
-
let(:send_from_committer_email) { true }
before do
@@ -756,7 +991,6 @@ describe Notify do
end
context "when the committer email domain is within the GitLab domain" do
-
before do
user.update_attribute(:email, "user@company.com")
user.confirm
@@ -774,7 +1008,6 @@ describe Notify do
end
context "when the committer email domain is not completely within the GitLab domain" do
-
before do
user.update_attribute(:email, "user@something.company.com")
user.confirm
@@ -792,7 +1025,6 @@ describe Notify do
end
context "when the committer email domain is outside the GitLab domain" do
-
before do
user.update_attribute(:email, "user@mpany.com")
user.confirm
@@ -817,8 +1049,9 @@ describe Notify do
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
+ let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
+ subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -831,10 +1064,6 @@ describe Notify do
expect(sender.address).to eq(gitlab_sender)
end
- it 'is sent to recipient' do
- is_expected.to deliver_to 'devs@company.name'
- end
-
it 'has the correct subject' do
is_expected.to have_subject /#{commits.first.title}/
end
@@ -843,13 +1072,12 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/
end
end
-
end
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb
new file mode 100644
index 00000000000..d6588efc486
--- /dev/null
+++ b/spec/mailers/previews/devise_mailer_preview.rb
@@ -0,0 +1,30 @@
+class DeviseMailerPreview < ActionMailer::Preview
+ def confirmation_instructions_for_signup
+ DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def confirmation_instructions_for_new_email
+ user = User.last
+ user.unconfirmed_email = 'unconfirmed@example.com'
+
+ DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ end
+
+ def reset_password_instructions
+ DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def unlock_instructions
+ DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def password_change
+ DeviseMailer.password_change(unsaved_user, {})
+ end
+
+ private
+
+ def unsaved_user
+ User.new(name: 'Jane Doe', email: 'jdoe@example.com')
+ end
+end
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 5a85cb501dd..93de5850ba2 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -146,8 +146,8 @@ shared_examples 'it should have Gmail Actions links' do
end
shared_examples 'it should not have Gmail Actions links' do
- it { is_expected.to_not have_body_text '<script type="application/ld+json">' }
- it { is_expected.to_not have_body_text /ViewAction/ }
+ it { is_expected.not_to have_body_text '<script type="application/ld+json">' }
+ it { is_expected.not_to have_body_text /ViewAction/ }
end
shared_examples 'it should show Gmail Actions View Issue link' do
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
new file mode 100644
index 00000000000..1acb5846fcf
--- /dev/null
+++ b/spec/models/ability_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Ability, lib: true do
+ describe '.users_that_can_read_project' do
+ context 'using a public project' do
+ it 'returns all the users' do
+ project = create(:project, :public)
+ user = build(:user)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it 'returns users that are administrators' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+
+ it 'returns internal users while skipping external users' do
+ user1 = build(:user)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are the project owner' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project).to receive(:owner).twice.and_return(user1)
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are project members' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project.team).to receive(:members).twice.and_return([user1])
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns an empty Array if all users are external users without access' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:project, :private) }
+
+ it 'returns users that are administrators' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+
+ it 'returns external users if they are the project owner' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project).to receive(:owner).twice.and_return(user1)
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are project members' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project.team).to receive(:members).twice.and_return([user1])
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns an empty Array if all users are internal users without access' do
+ user1 = build(:user)
+ user2 = build(:user)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+
+ it 'returns an empty Array if all users are external users without access' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 1ce22feed5c..2ea1320267c 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -20,6 +20,15 @@ describe ApplicationSetting, models: true do
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) }
+ describe 'disabled_oauth_sign_in_sources validations' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([:github])
+ end
+
+ it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) }
+ it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
+ end
+
it { is_expected.to validate_presence_of(:max_attachment_size) }
it do
@@ -31,6 +40,16 @@ describe ApplicationSetting, models: true do
it_behaves_like 'an object with email-formated attributes', :admin_notification_email do
subject { setting }
end
+
+ context 'repository storages inclussion' do
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ it { is_expected.to allow_value('custom').for(:repository_storage) }
+ it { is_expected.not_to allow_value('alternative').for(:repository_storage) }
+ end
end
context 'restricted signup domains' do
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
new file mode 100644
index 00000000000..cb3c592f8cd
--- /dev/null
+++ b/spec/models/award_emoji_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe AwardEmoji, models: true do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:awardable) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'modules' do
+ it { is_expected.to include_module(Participable) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:awardable) }
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:name) }
+
+ # To circumvent a bug in the shoulda matchers
+ describe "scoped uniqueness validation" do
+ it "rejects duplicate award emoji" do
+ user = create(:user)
+ issue = create(:issue)
+ create(:award_emoji, user: user, awardable: issue)
+ new_award = build(:award_emoji, user: user, awardable: issue)
+
+ expect(new_award).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index b5d356aa066..e8171788872 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1,18 +1,22 @@
require 'spec_helper'
describe Ci::Build, models: true do
- let(:project) { FactoryGirl.create :project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id)
+ end
+
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to validate_presence_of :ref }
it { is_expected.to respond_to :trace_html }
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 }
+ let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
+ let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
subject { Ci::Build.first_pending }
it { is_expected.to be_a(Ci::Build) }
@@ -37,32 +41,44 @@ describe Ci::Build, models: true do
subject { build.ignored? }
context 'if build is not allowed to fail' do
- before { build.allow_failure = false }
+ before do
+ build.allow_failure = false
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_falsey }
end
end
context 'if build is allowed to fail' do
- before { build.allow_failure = true }
+ before do
+ build.allow_failure = true
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_truthy }
end
@@ -76,7 +92,9 @@ describe Ci::Build, models: true do
context 'if build.trace contains text' do
let(:text) { 'example output' }
- before { build.trace = text }
+ before do
+ build.trace = text
+ end
it { is_expected.to include(text) }
it { expect(subject.length).to be >= text.length }
@@ -90,7 +108,7 @@ describe Ci::Build, models: true do
build.update_attributes(trace: token)
end
- it { is_expected.to_not include(token) }
+ it { is_expected.not_to include(token) }
end
end
@@ -98,7 +116,7 @@ describe Ci::Build, models: true do
# describe :timeout do
# subject { build.timeout }
#
- # it { is_expected.to eq(commit.project.timeout) }
+ # it { is_expected.to eq(pipeline.project.timeout) }
# end
describe '#options' do
@@ -125,13 +143,13 @@ describe Ci::Build, models: true do
describe '#project' do
subject { build.project }
- it { is_expected.to eq(commit.project) }
+ it { is_expected.to eq(pipeline.project) }
end
describe '#project_id' do
subject { build.project_id }
- it { is_expected.to eq(commit.project_id) }
+ it { is_expected.to eq(pipeline.project_id) }
end
describe '#project_name' do
@@ -189,7 +207,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(stage: 'stage') }
+ before do
+ build.update_attributes(stage: 'stage')
+ end
it { is_expected.to eq(predefined_variables + yaml_variables) }
@@ -200,7 +220,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(tag: true) }
+ before do
+ build.update_attributes(tag: true)
+ end
it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
end
@@ -219,8 +241,8 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) }
context 'and trigger variables' do
- let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
- let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger }
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
let(:trigger_variables) do
[
{ key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
@@ -258,38 +280,15 @@ describe Ci::Build, models: true do
end
end
- describe '#can_be_served?' do
- let(:runner) { FactoryGirl.create :ci_runner }
-
- before { build.project.runners << runner }
-
- context 'runner without tags' do
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build with tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
+ describe '#has_tags?' do
+ context 'when build has tags' do
+ subject { create(:ci_build, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
end
- context 'runner with tags' do
- before { runner.tag_list = ['bb', 'cc'] }
-
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'can handle build with matching tags' do
- build.tag_list = ['bb']
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build with not matching tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
+ context 'when build does not have tags' do
+ subject { create(:ci_build, tag_list: []) }
+ it { is_expected.not_to have_tags }
end
end
@@ -301,7 +300,7 @@ describe Ci::Build, models: true do
end
context 'if there are runner' do
- let(:runner) { FactoryGirl.create :ci_runner }
+ let(:runner) { create(:ci_runner) }
before do
build.project.runners << runner
@@ -321,10 +320,9 @@ describe Ci::Build, models: true do
end
it 'that cannot handle build' do
- expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false)
+ expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
is_expected.to be_falsey
end
-
end
end
@@ -333,12 +331,14 @@ describe Ci::Build, models: true do
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_truthy }
context "and there are specific runner" do
- let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago }
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
before do
build.project.runners << runner
@@ -352,7 +352,9 @@ describe Ci::Build, models: true do
%w(success failed canceled running).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_falsey }
end
@@ -363,16 +365,44 @@ describe Ci::Build, models: true do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
- before { build.update_attributes(artifacts_file: nil) }
+ before do
+ build.update_attributes(artifacts_file: nil)
+ end
+
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
let(:build) { create(:ci_build, :artifacts) }
it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ it { is_expected.to be_truthy }
+ end
end
end
+ describe '#artifacts_expired?' do
+ subject { build.artifacts_expired? }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
@@ -385,9 +415,8 @@ describe Ci::Build, models: true do
it { is_expected.to be_truthy }
end
end
-
describe '#repo_url' do
- let(:build) { FactoryGirl.create :ci_build }
+ let(:build) { create(:ci_build) }
let(:project) { build.project }
subject { build.repo_url }
@@ -400,11 +429,55 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
+ describe '#artifacts_expire_in' do
+ subject { build.artifacts_expire_in }
+ it { is_expected.to be_nil }
+
+ context 'when artifacts_expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before { build.artifacts_expire_at = expire_at }
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#artifacts_expire_in=' do
+ subject { build.artifacts_expire_in }
+
+ it 'when assigning valid duration' do
+ build.artifacts_expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+ is_expected.to be_nil
+ end
+
+ it 'when resseting value' do
+ build.artifacts_expire_in = nil
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#keep_artifacts!' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'to reset expire_at' do
+ build.keep_artifacts!
+
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ 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' }
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
+ let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
+ let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
+ let!(:staging) { create(:ci_build, pipeline: pipeline, 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
@@ -424,20 +497,19 @@ describe Ci::Build, models: true do
end
end
- def create_mr(build, commit, factory: :merge_request, created_at: Time.now)
- FactoryGirl.create(factory,
- source_project_id: commit.gl_project_id,
- target_project_id: commit.gl_project_id,
- source_branch: build.ref,
- created_at: created_at)
+ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
+ create(factory, source_project_id: pipeline.gl_project_id,
+ target_project_id: pipeline.gl_project_id,
+ source_branch: build.ref,
+ created_at: created_at)
end
describe '#merge_request' do
- context 'when a MR has a reference to the commit' do
+ context 'when a MR has a reference to the pipeline' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request)
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
- commits = [double(id: commit.sha)]
+ commits = [double(id: pipeline.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -447,19 +519,19 @@ describe Ci::Build, models: true do
end
end
- context 'when there is not a MR referencing the commit' do
+ context 'when there is not a MR referencing the pipeline' do
it 'returns nil' do
expect(build.merge_request).to be_nil
end
end
- context 'when more than one MR have a reference to the commit' do
+ context 'when more than one MR have a reference to the pipeline' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request)
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
@merge_request.close!
- @merge_request2 = create_mr(build, commit, factory: :merge_request)
+ @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
- commits = [double(id: commit.sha)]
+ commits = [double(id: pipeline.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(@merge_request2).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
@@ -472,11 +544,11 @@ describe Ci::Build, models: true do
context 'when a Build is created after the MR' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs)
- commit2 = FactoryGirl.create :ci_commit, project: project
- @build2 = FactoryGirl.create :ci_build, commit: commit2
+ @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
+ pipeline2 = create(:ci_pipeline, project: project)
+ @build2 = create(:ci_build, pipeline: pipeline2)
- commits = [double(id: commit.sha), double(id: commit2.sha)]
+ commits = [double(id: pipeline.sha), double(id: pipeline2.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -506,7 +578,7 @@ describe Ci::Build, models: true do
end
it 'should set erase date' do
- expect(build.erased_at).to_not be_falsy
+ expect(build.erased_at).not_to be_falsy
end
end
@@ -529,7 +601,9 @@ describe Ci::Build, models: true do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
describe '#erase' do
- before { build.erase(erased_by: user) }
+ before do
+ build.erase(erased_by: user)
+ end
context 'erased by user' do
let!(:user) { create(:user, username: 'eraser') }
@@ -566,7 +640,9 @@ describe Ci::Build, models: true do
end
context 'build has been erased' do
- before { build.erase }
+ before do
+ build.erase
+ end
it { is_expected.to be true }
end
@@ -574,14 +650,40 @@ describe Ci::Build, models: true do
context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) }
- before { build.remove_artifacts_metadata! }
+ before do
+ build.remove_artifacts_metadata!
+ end
describe '#erase' do
it 'should not raise error' do
- expect { build.erase }.to_not raise_error
+ expect { build.erase }.not_to raise_error
end
end
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(build.commit).to eq project.commit
+ end
+ end
+
+ describe '#retryable?' do
+ context 'when build is running' do
+ before { build.run! }
+
+ it 'should return false' do
+ expect(build.retryable?).to be false
+ end
+ end
+
+ context 'when build is finished' do
+ before { build.success! }
+
+ it 'should return true' do
+ expect(build.retryable?).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
deleted file mode 100644
index dc071ad1c90..00000000000
--- a/spec/models/ci/commit_spec.rb
+++ /dev/null
@@ -1,404 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Commit, models: true do
- let(:project) { FactoryGirl.create :empty_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
-
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:statuses) }
- it { is_expected.to have_many(:trigger_requests) }
- it { is_expected.to have_many(:builds) }
- it { is_expected.to validate_presence_of :sha }
- it { is_expected.to validate_presence_of :status }
- it { is_expected.to delegate_method(:stages).to(:statuses) }
-
- it { is_expected.to respond_to :git_author_name }
- it { is_expected.to respond_to :git_author_email }
- it { is_expected.to respond_to :short_sha }
-
- describe :valid_commit_sha do
- context 'commit.sha can not start with 00000000' do
- before do
- commit.sha = '0' * 40
- commit.valid_commit_sha
- end
-
- it('commit errors should not be empty') { expect(commit.errors).not_to be_empty }
- end
- end
-
- describe :short_sha do
- subject { commit.short_sha }
-
- it 'has 8 items' do
- expect(subject.size).to eq(8)
- end
- it { expect(commit.sha).to start_with(subject) }
- end
-
- describe :create_next_builds do
- end
-
- describe :retried do
- subject { commit.retried }
-
- before do
- @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
- @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
- end
-
- it 'returns old builds' do
- is_expected.to contain_exactly(@commit1)
- end
- end
-
- describe :create_builds do
- let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false }
-
- def create_builds(trigger_request = nil)
- commit.create_builds(nil, trigger_request)
- end
-
- def create_next_builds
- commit.create_next_builds(commit.builds.order(:id).last)
- end
-
- it 'creates builds' do
- expect(create_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(2)
-
- expect(create_next_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(4)
-
- expect(create_next_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(5)
-
- expect(create_next_builds).to be_falsey
- 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
- {
- stages: ["build", "test", "test_failure", "deploy", "cleanup"],
- build: {
- stage: "build",
- script: "BUILD",
- },
- test: {
- stage: "test",
- script: "TEST",
- },
- test_failure: {
- stage: "test_failure",
- script: "ON test failure",
- when: "on_failure",
- },
- deploy: {
- stage: "deploy",
- script: "PUBLISH",
- },
- cleanup: {
- stage: "cleanup",
- script: "TIDY UP",
- when: "always",
- }
- }
- end
-
- before do
- stub_ci_commit_yaml_file(YAML.dump(yaml))
- end
-
- context 'when builds are successful' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- commit.reload
- expect(commit.status).to eq('success')
- end
- end
-
- context 'when test job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when test and test_failure jobs fail' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when deploy job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when build is canceled in the second stage' do
- it 'does not schedule builds after build has been canceled' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.running_or_pending).to_not be_empty
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:cancel)
-
- expect(commit.builds.running_or_pending).to be_empty
- expect(commit.reload.status).to eq('canceled')
- end
- end
- end
- end
-
- describe "#finished_at" do
- let(:commit) { FactoryGirl.create :ci_commit }
-
- it "returns finished_at of latest build" do
- build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60
- FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
-
- expect(commit.finished_at.to_i).to eq(build.finished_at.to_i)
- end
-
- it "returns nil if there is no finished build" do
- FactoryGirl.create :ci_not_started_build, commit: commit
-
- expect(commit.finished_at).to be_nil
- end
- end
-
- describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
-
- it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- FactoryGirl.create :ci_build, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, commit: commit
- expect(commit.coverage).to be_nil
- end
- end
-
- describe '#retryable?' do
- subject { commit.retryable? }
-
- context 'no failed builds' do
- before do
- FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success'
- end
-
- it 'be not retryable' do
- is_expected.to be_falsey
- end
- end
-
- context 'with failed builds' do
- before do
- FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running'
- FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed'
- end
-
- it 'be retryable' do
- is_expected.to be_truthy
- end
- end
- end
-
- describe '#stages' do
- let(:commit2) { FactoryGirl.create :ci_commit, project: project }
- subject { CommitStatus.where(commit: [commit, commit2]).stages }
-
- before do
- FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0
- end
-
- it 'return all stages' do
- is_expected.to eq(%w(build test))
- end
- end
-
- describe '#update_state' do
- it 'execute update_state after touching object' do
- expect(commit).to receive(:update_state).and_return(true)
- commit.touch
- end
-
- context 'dependent objects' do
- let(:commit_status) { build :commit_status, commit: commit }
-
- it 'execute update_state after saving dependent object' do
- expect(commit).to receive(:update_state).and_return(true)
- commit_status.save
- end
- end
-
- context 'update state' do
- let(:current) { Time.now.change(usec: 0) }
- let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 }
-
- before do
- build
- end
-
- [:status, :started_at, :finished_at, :duration].each do |param|
- it "update #{param}" do
- expect(commit.send(param)).to eq(build.send(param))
- end
- end
- end
- end
-
- describe '#branch?' do
- subject { commit.branch? }
-
- context 'is not a tag' do
- before do
- commit.tag = false
- end
-
- it 'return true when tag is set to false' do
- is_expected.to be_truthy
- end
- end
-
- context 'is not a tag' do
- before do
- commit.tag = true
- end
-
- it 'return false when tag is set to true' do
- is_expected.to be_falsey
- end
- end
- end
-end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
new file mode 100644
index 00000000000..34507cf5083
--- /dev/null
+++ b/spec/models/ci/pipeline_spec.rb
@@ -0,0 +1,416 @@
+require 'spec_helper'
+
+describe Ci::Pipeline, models: true do
+ let(:project) { FactoryGirl.create :empty_project }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:statuses) }
+ it { is_expected.to have_many(:trigger_requests) }
+ it { is_expected.to have_many(:builds) }
+ it { is_expected.to validate_presence_of :sha }
+ it { is_expected.to validate_presence_of :status }
+
+ it { is_expected.to respond_to :git_author_name }
+ it { is_expected.to respond_to :git_author_email }
+ it { is_expected.to respond_to :short_sha }
+
+ describe :valid_commit_sha do
+ context 'commit.sha can not start with 00000000' do
+ before do
+ pipeline.sha = '0' * 40
+ pipeline.valid_commit_sha
+ end
+
+ it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty }
+ end
+ end
+
+ describe :short_sha do
+ subject { pipeline.short_sha }
+
+ it 'has 8 items' do
+ expect(subject.size).to eq(8)
+ end
+ it { expect(pipeline.sha).to start_with(subject) }
+ end
+
+ describe :create_next_builds do
+ end
+
+ describe :retried do
+ subject { pipeline.retried }
+
+ before do
+ @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ end
+
+ it 'returns old builds' do
+ is_expected.to contain_exactly(@build1)
+ end
+ end
+
+ describe :create_builds do
+ let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
+
+ def create_builds(trigger_request = nil)
+ pipeline.create_builds(nil, trigger_request)
+ end
+
+ def create_next_builds
+ pipeline.create_next_builds(pipeline.builds.order(:id).last)
+ end
+
+ it 'creates builds' do
+ expect(create_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(2)
+
+ expect(create_next_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(4)
+
+ expect(create_next_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(5)
+
+ expect(create_next_builds).to be_falsey
+ 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_pipeline_yaml_file(YAML.dump(yaml))
+ create_builds
+ end
+
+ it 'properly schedules builds' do
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed')
+ end
+ end
+
+ context 'properly creates builds when "when" is defined' do
+ let(:yaml) do
+ {
+ stages: ["build", "test", "test_failure", "deploy", "cleanup"],
+ build: {
+ stage: "build",
+ script: "BUILD",
+ },
+ test: {
+ stage: "test",
+ script: "TEST",
+ },
+ test_failure: {
+ stage: "test_failure",
+ script: "ON test failure",
+ when: "on_failure",
+ },
+ deploy: {
+ stage: "deploy",
+ script: "PUBLISH",
+ },
+ cleanup: {
+ stage: "cleanup",
+ script: "TIDY UP",
+ when: "always",
+ }
+ }
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(yaml))
+ end
+
+ context 'when builds are successful' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('success')
+ end
+ end
+
+ context 'when test job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when test and test_failure jobs fail' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when deploy job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when build is canceled in the second stage' do
+ it 'does not schedule builds after build has been canceled' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.running_or_pending).not_to be_empty
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:cancel)
+
+ expect(pipeline.builds.running_or_pending).to be_empty
+ expect(pipeline.reload.status).to eq('canceled')
+ end
+ end
+ end
+
+ context 'when no builds created' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
+ end
+
+ it 'returns false' do
+ expect(pipeline.create_builds(nil)).to be_falsey
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+
+ describe "#finished_at" do
+ let(:pipeline) { FactoryGirl.create :ci_pipeline }
+
+ it "returns finished_at of latest build" do
+ build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
+ FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
+
+ expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
+ end
+
+ it "returns nil if there is no finished build" do
+ FactoryGirl.create :ci_not_started_build, pipeline: pipeline
+
+ expect(pipeline.finished_at).to be_nil
+ end
+ end
+
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+
+ it "calculates average when there are two builds with coverage" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one with nil" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one is retried" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there is one build without coverage" do
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to be_nil
+ end
+ end
+
+ describe '#retryable?' do
+ subject { pipeline.retryable? }
+
+ context 'no failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ end
+
+ it 'be not retryable' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'with failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
+ FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+ end
+
+ it 'be retryable' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
+ describe '#stages' do
+ let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
+ subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
+
+ before do
+ FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
+ FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ end
+
+ it 'return all stages' do
+ is_expected.to eq(%w(build test))
+ end
+ end
+
+ describe '#update_state' do
+ it 'execute update_state after touching object' do
+ expect(pipeline).to receive(:update_state).and_return(true)
+ pipeline.touch
+ end
+
+ context 'dependent objects' do
+ let(:commit_status) { build :commit_status, pipeline: pipeline }
+
+ it 'execute update_state after saving dependent object' do
+ expect(pipeline).to receive(:update_state).and_return(true)
+ commit_status.save
+ end
+ end
+
+ context 'update state' do
+ let(:current) { Time.now.change(usec: 0) }
+ let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
+
+ before do
+ build
+ end
+
+ [:status, :started_at, :finished_at, :duration].each do |param|
+ it "update #{param}" do
+ expect(pipeline.send(param)).to eq(build.send(param))
+ end
+ end
+ end
+ end
+
+ describe '#branch?' do
+ subject { pipeline.branch? }
+
+ context 'is not a tag' do
+ before do
+ pipeline.tag = false
+ end
+
+ it 'return true when tag is set to false' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'is not a tag' do
+ before do
+ pipeline.tag = true
+ end
+
+ it 'return false when tag is set to true' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
deleted file mode 100644
index 95fc160b238..00000000000
--- a/spec/models/ci/runner_project_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe Ci::RunnerProject, models: true do
- pending "add some examples to (or delete) #{__FILE__}"
-end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index eaa94228922..ef65eb99328 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,35 +1,55 @@
require 'spec_helper'
describe Ci::Runner, models: true do
+ describe 'validation' do
+ context 'when runner is not allowed to pick untagged jobs' do
+ context 'when runner does not have tags' do
+ it 'is not valid' do
+ runner = build(:ci_runner, tag_list: [], run_untagged: false)
+ expect(runner).to be_invalid
+ end
+ end
+
+ context 'when runner has tags' do
+ it 'is valid' do
+ runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false)
+ expect(runner).to be_valid
+ end
+ end
+ end
+ end
+
describe '#display_name' do
- it 'should return the description if it has a value' do
+ it 'returns the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end
- it 'should return the token if it does not have a description' do
+ it 'returns the token if it does not have a description' do
runner = FactoryGirl.create(:ci_runner)
expect(runner.display_name).to eq runner.description
end
- it 'should return the token if the description is an empty string' do
+ it 'returns the token if the description is an empty string' do
runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
- describe :assign_to do
+ describe '#assign_to' do
let!(:project) { FactoryGirl.create :empty_project }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
- before { shared_runner.assign_to(project) }
+ before do
+ shared_runner.assign_to(project)
+ end
it { expect(shared_runner).to be_specific }
it { expect(shared_runner.projects).to eq([project]) }
it { expect(shared_runner.only_for?(project)).to be_truthy }
end
- describe :online do
+ describe '.online' do
subject { Ci::Runner.online }
before do
@@ -40,60 +60,269 @@ describe Ci::Runner, models: true do
it { is_expected.to eq([@runner2])}
end
- describe :online? do
+ describe '#online?' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
subject { runner.online? }
context 'never contacted' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to be_falsey }
end
context 'contacted long time ago time' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to be_falsey }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to be_truthy }
end
end
- describe :status do
+ describe '#can_pick?' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ build.project.runners << runner
+ end
+
+ context 'when runner does not have tags' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build with tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner has tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ shared_examples 'tagged build picker' do
+ it 'can handle build with matching tags' do
+ build.tag_list = ['bb']
+
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner can pick untagged jobs' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+ end
+
+ context 'when runner is locked' do
+ before do
+ runner.locked = true
+ end
+
+ shared_examples 'locked build picker' do
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ it 'cannot handle it for builds without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+
+ context 'when serving the same project' do
+ it 'can handle it' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'can handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+ end
+ end
+
+ context 'serving a different project' do
+ before do
+ runner.runner_projects.destroy_all
+ end
+
+ it 'cannot handle it' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'cannot handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+ end
+ end
+
+ describe '#status' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status }
context 'never connected' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to eq(:not_connected) }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to eq(:online) }
end
context 'contacted long time ago' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to eq(:offline) }
end
context 'inactive' do
- before { runner.active = false }
+ before do
+ runner.active = false
+ end
it { is_expected.to eq(:paused) }
end
end
+ describe '.assignable_for' do
+ let(:runner) { create(:ci_runner) }
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project) }
+
+ before do
+ project.runners << runner
+ end
+
+ context 'with shared runners' do
+ before do
+ runner.update(is_shared: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give shared runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with unlocked runner' do
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does give a specific runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to contain_exactly(runner) }
+ end
+ end
+
+ context 'with locked runner' do
+ before do
+ runner.update(locked: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give a locked runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_runner)
@@ -114,7 +343,19 @@ describe Ci::Runner, models: true do
end
end
- describe '#search' do
+ describe '#has_tags?' do
+ context 'when runner has tags' do
+ subject { create(:ci_runner, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
+ end
+
+ context 'when runner does not have tags' do
+ subject { create(:ci_runner, tag_list: []) }
+ it { is_expected.not_to have_tags }
+ end
+ end
+
+ describe '.search' do
let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index c712d211b0f..98f60087cf5 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -23,7 +23,7 @@ describe Ci::Variable, models: true do
end
it 'fails to decrypt if iv is incorrect' do
- subject.encrypted_value_iv = nil
+ subject.encrypted_value_iv = SecureRandom.hex
subject.instance_variable_set(:@value, nil)
expect { subject.value }.
to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 9307d97e214..384a38ebc69 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -24,6 +24,16 @@ describe CommitRange, models: true do
expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end
+ describe '#initialize' do
+ it 'does not modify strings in-place' do
+ input = "#{sha_from}...#{sha_to} "
+
+ described_class.new(input, project)
+
+ expect(input).to eq("#{sha_from}...#{sha_to} ")
+ end
+ end
+
describe '#to_s' do
it 'is correct for three-dot syntax' do
expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
@@ -135,4 +145,28 @@ describe CommitRange, models: true do
end
end
end
+
+ describe '#has_been_reverted?' do
+ it 'returns true if the commit has been reverted' do
+ issue = create(:issue)
+
+ create(:note_on_issue,
+ noteable: issue,
+ system: true,
+ note: commit1.revert_description,
+ project: issue.project)
+
+ expect_any_instance_of(Commit).to receive(:reverts_commit?).
+ with(commit1).
+ and_return(true)
+
+ expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
+ end
+
+ it 'returns false a commit has not been reverted' do
+ issue = create(:issue)
+
+ expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ad47e338a33..ba02d5fe977 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Commit, models: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:commit) { project.commit }
describe 'modules' do
@@ -56,7 +56,7 @@ describe Commit, models: true do
end
it "does not truncates a message with a newline after 80 but less 100 characters" do
- message =<<eos
+ message = <<eos
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit.
Vivamus egestas lacinia lacus, sed rutrum mauris.
eos
@@ -171,4 +171,52 @@ eos
describe '#status' do
# TODO: kamil
end
+
+ describe '#participants' do
+ let(:user1) { build(:user) }
+ let(:user2) { build(:user) }
+
+ let!(:note1) do
+ create(:note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'foo')
+ end
+
+ let!(:note2) do
+ create(:note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'bar')
+ end
+
+ before do
+ allow(commit).to receive(:author).and_return(user1)
+ allow(commit).to receive(:committer).and_return(user2)
+ end
+
+ it 'includes the commit author' do
+ expect(commit.participants).to include(commit.author)
+ end
+
+ it 'includes the committer' do
+ expect(commit.participants).to include(commit.committer)
+ end
+
+ it 'includes the authors of the commit notes' do
+ expect(commit.participants).to include(note1.author, note2.author)
+ end
+ end
+
+ describe '#uri_type' do
+ it 'returns the URI type at the given path' do
+ expect(commit.uri_type('files/html')).to be(:tree)
+ expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(commit.uri_type('files/js/application.js')).to be(:blob)
+ end
+
+ it "returns nil if the path doesn't exists" do
+ expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 434e58cfd06..96397d7c8a9 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,19 +1,24 @@
require 'spec_helper'
describe CommitStatus, models: true do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:commit_status) { FactoryGirl.create :commit_status, commit: commit }
+ let(:project) { create(:project) }
- it { is_expected.to belong_to(:commit) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ end
+
+ let(:commit_status) { create(:commit_status, pipeline: pipeline) }
+
+ it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
- it { is_expected.to delegate_method(:sha).to(:commit) }
- it { is_expected.to delegate_method(:short_sha).to(:commit) }
-
+ it { is_expected.to delegate_method(:sha).to(:pipeline) }
+ it { is_expected.to delegate_method(:short_sha).to(:pipeline) }
+
it { is_expected.to respond_to :success? }
it { is_expected.to respond_to :failed? }
it { is_expected.to respond_to :running? }
@@ -116,16 +121,16 @@ describe CommitStatus, models: true do
it { is_expected.to be > 0.0 }
end
end
-
+
describe :latest do
subject { CommitStatus.latest.order(:id) }
before do
- @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success'
- @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success'
- @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success'
+ @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success'
+ @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
end
it 'return unique statuses' do
@@ -137,11 +142,11 @@ describe CommitStatus, models: true do
subject { CommitStatus.running_or_pending.order(:id) }
before do
- @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
- @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
- @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
+ @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed'
+ @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
end
it 'return statuses that are running or pending' do
@@ -152,17 +157,17 @@ describe CommitStatus, models: true do
describe '#before_sha' do
subject { commit_status.before_sha }
- context 'when no before_sha is set for ci::commit' do
- before { commit.before_sha = nil }
+ context 'when no before_sha is set for pipeline' do
+ before { pipeline.before_sha = nil }
it 'return blank sha' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
end
end
- context 'for before_sha set for ci::commit' do
+ context 'for before_sha set for pipeline' do
let(:value) { '1234' }
- before { commit.before_sha = value }
+ before { pipeline.before_sha = value }
it 'return the set value' do
is_expected.to eq(value)
@@ -172,14 +177,14 @@ describe CommitStatus, models: true do
describe '#stages' do
before do
- FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success'
- FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed'
- FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running'
- FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success'
end
context 'stages list' do
- subject { CommitStatus.where(commit: commit).stages }
+ subject { CommitStatus.where(pipeline: pipeline).stages }
it 'return ordered list of stages' do
is_expected.to eq(%w(build test deploy))
@@ -187,7 +192,7 @@ describe CommitStatus, models: true do
end
context 'stages with statuses' do
- subject { CommitStatus.where(commit: commit).stages_status }
+ subject { CommitStatus.where(pipeline: pipeline).stages_status }
it 'return list of stages with statuses' do
is_expected.to eq({
@@ -198,4 +203,10 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(commit_status.commit).to eq project.commit
+ end
+ end
end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
new file mode 100644
index 00000000000..96eee0e8bdd
--- /dev/null
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe AccessRequestable do
+ describe 'Group' do
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(group.request_access(user)).to be_a(GroupMember) }
+ it { expect(group.request_access(user).user).to eq(user) }
+ end
+
+ describe '#access_requested?' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before { group.request_access(user) }
+
+ it { expect(group.requesters.exists?(user_id: user)).to be_truthy }
+ end
+ end
+
+ describe 'Project' do
+ describe '#request_access' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(project.request_access(user)).to be_a(ProjectMember) }
+ end
+
+ describe '#access_requested?' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ before { project.request_access(user) }
+
+ it { expect(project.requesters.exists?(user_id: user)).to be_truthy }
+ end
+ end
+end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
new file mode 100644
index 00000000000..a371c4a18a9
--- /dev/null
+++ b/spec/models/concerns/awardable_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Issue, "Awardable" do
+ let!(:issue) { create(:issue) }
+ let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
+
+ describe "Associations" do
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+ end
+
+ describe "ClassMethods" do
+ let!(:issue2) { create(:issue) }
+
+ before do
+ create(:award_emoji, awardable: issue2)
+ end
+
+ it "orders on upvotes" do
+ expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
+ end
+
+ it "orders on downvotes" do
+ expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
+ end
+ end
+
+ describe "#upvotes" do
+ it "counts the number of upvotes" do
+ expect(issue.upvotes).to be 0
+ end
+ end
+
+ describe "#downvotes" do
+ it "counts the number of downvotes" do
+ expect(issue.downvotes).to be 1
+ end
+ end
+
+ describe "#toggle_award_emoji" do
+ it "adds an emoji if it isn't awarded yet" do
+ expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it "toggles already awarded emoji" do
+ expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4a4cd093435..60e4bbc8564 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -10,6 +10,20 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+
+ context 'Notes' do
+ let!(:note) { create(:note, noteable: issue, project: issue.project) }
+ let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) }
+
+ it 'indicates if the notes have their authors loaded' do
+ expect(issue.notes).not_to be_authors_loaded
+ expect(scoped_issue.notes).to be_authors_loaded
+ end
+ end
+ end
+
+ describe 'Included modules' do
+ it { is_expected.to include_module(Awardable) }
end
describe "Validation" do
@@ -114,6 +128,48 @@ describe Issue, "Issuable" do
end
end
+ describe "#sort" do
+ let(:project) { build_stubbed(:empty_project) }
+
+ context "by milestone due date" do
+ # Correct order is:
+ # Issues/MRs with milestones ordered by date
+ # Issues/MRs with milestones without dates
+ # Issues/MRs without milestones
+
+ let!(:issue) { create(:issue, project: project) }
+ let!(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let!(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let!(:issue1) { create(:issue, project: project, milestone: early_milestone) }
+ let!(:issue2) { create(:issue, project: project, milestone: late_milestone) }
+ let!(:issue3) { create(:issue, project: project) }
+
+ it "sorts desc" do
+ issues = project.issues.sort('milestone_due_desc')
+ expect(issues).to match_array([issue2, issue1, issue, issue3])
+ end
+
+ it "sorts asc" do
+ issues = project.issues.sort('milestone_due_asc')
+ expect(issues).to match_array([issue1, issue2, issue, issue3])
+ end
+ end
+
+ context 'when all of the results are level on the sort key' do
+ let!(:issues) do
+ 10.times { create(:issue, project: project) }
+ end
+
+ it 'has no duplicates across pages' do
+ sorted_issue_ids = 1.upto(10).map do |i|
+ project.issues.sort('milestone_due_desc').page(i).per(1).first.id
+ end
+
+ expect(sorted_issue_ids).to eq(sorted_issue_ids.uniq)
+ end
+ 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([]) }
@@ -160,12 +216,11 @@ describe Issue, "Issuable" do
let(:data) { issue.to_hook_data(user) }
let(:project) { issue.project }
-
it "returns correct hook data" do
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)
+ expect(data).not_to have_key(:assignee)
end
context "issue is assigned" do
@@ -199,12 +254,42 @@ describe Issue, "Issuable" do
end
end
+ describe '#labels_array' do
+ let(:project) { create(:project) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:issue) { create(:issue, project: project) }
+
+ before(:each) do
+ issue.labels << bug
+ end
+
+ it 'loads the association and returns it as an array' do
+ expect(issue.reload.labels_array).to eq([bug])
+ end
+ end
+
+ describe '#user_notes_count' do
+ let(:project) { create(:project) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+
+ before do
+ create_list(:note, 3, noteable: issue1, project: project)
+ create_list(:note, 6, noteable: issue2, project: project)
+ end
+
+ it 'counts the user notes' do
+ expect(issue1.user_notes_count).to be(3)
+ expect(issue2.user_notes_count).to be(6)
+ end
+ end
+
describe "votes" do
+ let(:project) { issue.project }
+
before do
- author = create :user
- project = create :empty_project
- issue.notes.awards.create!(note: "thumbsup", author: author, project: project)
- issue.notes.awards.create!(note: "thumbsdown", author: author, project: project)
+ create(:award_emoji, :upvote, awardable: issue)
+ create(:award_emoji, :downvote, awardable: issue)
end
it "returns correct values" do
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index cb33edde820..0344dae8b5d 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -29,6 +29,43 @@ describe Issue, "Mentionable" do
it { is_expected.not_to include(user2) }
end
+ describe '#referenced_mentionables' do
+ context 'with an issue on a private project' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:public_issue) { create(:issue, project: project) }
+ let(:private_project) { create(:empty_project, :private) }
+ let(:private_issue) { create(:issue, project: private_project) }
+ let(:user) { create(:user) }
+
+ def referenced_issues(current_user)
+ text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
+
+ issue.referenced_mentionables(current_user, text)
+ end
+
+ context 'when the current user can see the issue' do
+ before { private_project.team << [user, Gitlab::Access::DEVELOPER] }
+
+ it 'includes the reference' do
+ expect(referenced_issues(user)).to contain_exactly(private_issue, public_issue)
+ end
+ end
+
+ context 'when the current user cannot see the issue' do
+ it 'does not include the reference' do
+ expect(referenced_issues(user)).to contain_exactly(public_issue)
+ end
+ end
+
+ context 'when there is no current user' do
+ it 'does not include the reference' do
+ expect(referenced_issues(nil)).to contain_exactly(public_issue)
+ end
+ end
+ end
+ end
+
describe '#create_cross_references!' do
let(:project) { create(:project) }
let(:author) { double('author') }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 47c3be673c5..7e9ab8940cf 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -5,6 +5,7 @@ describe Milestone, 'Milestoneish' do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -21,6 +22,7 @@ describe Milestone, 'Milestoneish' do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
describe '#closed_items_count' do
@@ -28,6 +30,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.closed_items_count(non_member)).to eq 2
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.closed_items_count(guest)).to eq 2
+ end
+
it 'should count confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
@@ -50,6 +56,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.total_items_count(non_member)).to eq 4
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.total_items_count(guest)).to eq 4
+ end
+
it 'should count confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
@@ -85,6 +95,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.percent_complete(non_member)).to eq 50
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.percent_complete(guest)).to eq 50
+ end
+
it 'should count confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
new file mode 100644
index 00000000000..a9f4ef9ee5e
--- /dev/null
+++ b/spec/models/concerns/participable_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Participable, models: true do
+ let(:model) do
+ Class.new do
+ include Participable
+ end
+ end
+
+ describe '.participant' do
+ it 'adds the participant attributes to the existing list' do
+ model.participant(:foo)
+ model.participant(:bar)
+
+ expect(model.participant_attrs).to eq([:foo, :bar])
+ end
+ end
+
+ describe '#participants' do
+ it 'returns the list of participants' do
+ model.participant(:foo)
+ model.participant(:bar)
+
+ user1 = build(:user)
+ user2 = build(:user)
+ user3 = build(:user)
+ project = build(:project, :public)
+ instance = model.new
+
+ expect(instance).to receive(:foo).and_return(user2)
+ expect(instance).to receive(:bar).and_return(user3)
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ participants = instance.participants(user1)
+
+ expect(participants).to include(user2)
+ expect(participants).to include(user3)
+ end
+
+ it 'caches the raw list of participants' do
+ instance = model.new
+ user1 = build(:user)
+
+ expect(instance).to receive(:raw_participants).once
+
+ instance.participants(user1)
+ instance.participants(user1)
+ end
+
+ it 'supports attributes returning another Participable' do
+ other_model = Class.new { include Participable }
+
+ other_model.participant(:bar)
+ model.participant(:foo)
+
+ instance = model.new
+ other = other_model.new
+ user1 = build(:user)
+ user2 = build(:user)
+ project = build(:project, :public)
+
+ expect(instance).to receive(:foo).and_return(other)
+ expect(other).to receive(:bar).and_return(user2)
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ expect(instance.participants(user1)).to eq([user2])
+ end
+
+ context 'when using a Proc as an attribute' do
+ it 'calls the supplied Proc' do
+ user1 = build(:user)
+ project = build(:project, :public)
+
+ user_arg = nil
+ ext_arg = nil
+
+ model.participant -> (user, ext) do
+ user_arg = user
+ ext_arg = ext
+ end
+
+ instance = model.new
+
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ instance.participants(user1)
+
+ expect(user_arg).to eq(user1)
+ expect(ext_arg).to be_an_instance_of(Gitlab::ReferenceExtractor)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
index 6445e29c3ef..c3af7a0960f 100644
--- a/spec/models/concerns/strip_attribute_spec.rb
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -16,5 +16,4 @@ describe Milestone, "StripAttribute" do
it { expect(milestone.title).to eq('8.3') }
end
-
end
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index e31fdb0bffb..b7fc5a92497 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -44,6 +44,16 @@ describe Subscribable, 'Subscribable' do
end
end
+ describe '#subscribe' do
+ it 'subscribes the given user' do
+ expect(resource.subscribed?(user)).to be_falsey
+
+ resource.subscribe(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)
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 30c0a04b840..9e8ebc56a31 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -28,14 +28,14 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
context 'token is not generated yet' do
describe 'token field accessor' do
subject { described_class.new.send(token_field) }
- it { is_expected.to_not be_blank }
+ it { is_expected.not_to be_blank }
end
describe 'ensured token' do
subject { described_class.new.send("ensure_#{token_field}") }
it { is_expected.to be_a String }
- it { is_expected.to_not be_blank }
+ it { is_expected.not_to be_blank }
end
describe 'ensured! token' do
@@ -49,7 +49,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
context 'token is generated' do
before { subject.send("reset_#{token_field}!") }
- it 'persists a new token 'do
+ it 'persists a new token' do
expect(subject.send(:read_attribute, token_field)).to be_a String
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
new file mode 100644
index 00000000000..b273018707f
--- /dev/null
+++ b/spec/models/deployment_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Deployment, models: true do
+ subject { build(:deployment) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:environment) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:deployable) }
+
+ it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
+ it { is_expected.to delegate_method(:commit).to(:project) }
+ it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
+
+ it { is_expected.to validate_presence_of(:ref) }
+ it { is_expected.to validate_presence_of(:sha) }
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
new file mode 100644
index 00000000000..af8e890ca95
--- /dev/null
+++ b/spec/models/diff_note_spec.rb
@@ -0,0 +1,191 @@
+require 'spec_helper'
+
+describe DiffNote, models: true do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:commit) { project.commit(sample_commit.id) }
+
+ let(:path) { "files/ruby/popen.rb" }
+
+ let!(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let!(:new_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: 16,
+ new_line: 22,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+
+ describe "#position=" do
+ context "when provided a string" do
+ it "sets the position" do
+ subject.position = new_position.to_json
+
+ expect(subject.position).to eq(new_position)
+ end
+ end
+
+ context "when provided a hash" do
+ it "sets the position" do
+ subject.position = new_position.to_h
+
+ expect(subject.position).to eq(new_position)
+ end
+ end
+
+ context "when provided a position object" do
+ it "sets the position" do
+ subject.position = new_position
+
+ expect(subject.position).to eq(new_position)
+ end
+ end
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file
+
+ expect(diff_file.old_path).to eq(position.old_path)
+ expect(diff_file.new_path).to eq(position.new_path)
+ expect(diff_file.diff_refs).to eq(position.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(position.new_line)
+ expect(diff_line.text).to eq("+ vars = {")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15)
+
+ expect(subject.line_code).to eq(line_code)
+ end
+ end
+
+ describe "#for_line?" do
+ context "when provided the correct diff line" do
+ it "returns true" do
+ expect(subject.for_line?(subject.diff_line)).to be true
+ end
+ end
+
+ context "when provided a different diff line" do
+ it "returns false" do
+ some_line = subject.diff_file.diff_lines.first
+
+ expect(subject.for_line?(some_line)).to be false
+ end
+ end
+ end
+
+ describe "#active?" do
+ context "when noteable is a commit" do
+ subject { create(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns true" do
+ expect(subject.active?).to be true
+ end
+ end
+
+ context "when noteable is a merge request" do
+ context "when the merge request's diff refs match that of the diff note" do
+ it "returns true" do
+ expect(subject.active?).to be true
+ end
+ end
+
+ context "when the merge request's diff refs don't match that of the diff note" do
+ before do
+ allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
+ end
+
+ it "returns false" do
+ expect(subject.active?).to be false
+ end
+ end
+ end
+ end
+
+ describe "creation" do
+ describe "updating of position" do
+ context "when noteable is a commit" do
+ let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
+
+ it "doesn't use the DiffPositionUpdateService" do
+ expect(Notes::DiffPositionUpdateService).not_to receive(:new)
+
+ diff_note
+ end
+
+ it "doesn't update the position" do
+ diff_note
+
+ expect(diff_note.original_position).to eq(position)
+ expect(diff_note.position).to eq(position)
+ end
+ end
+
+ context "when noteable is a merge request" do
+ let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+
+ context "when the note is active" do
+ it "doesn't use the DiffPositionUpdateService" do
+ expect(Notes::DiffPositionUpdateService).not_to receive(:new)
+
+ diff_note
+ end
+
+ it "doesn't update the position" do
+ diff_note
+
+ expect(diff_note.original_position).to eq(position)
+ expect(diff_note.position).to eq(position)
+ end
+ end
+
+ context "when the note is outdated" do
+ before do
+ allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
+ end
+
+ it "uses the DiffPositionUpdateService" do
+ service = instance_double("Notes::DiffPositionUpdateService")
+ expect(Notes::DiffPositionUpdateService).to receive(:new).with(
+ project,
+ nil,
+ old_diff_refs: position.diff_refs,
+ new_diff_refs: commit.diff_refs,
+ paths: [path]
+ ).and_return(service)
+ expect(service).to receive(:execute)
+
+ diff_note
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 5d0bd31db5a..d9df9e0f907 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -1,11 +1,9 @@
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/environment_spec.rb b/spec/models/environment_spec.rb
new file mode 100644
index 00000000000..7629af6a570
--- /dev/null
+++ b/spec/models/environment_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Environment, models: true do
+ let(:environment) { create(:environment) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:deployments) }
+
+ it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:name).is_within(0..255) }
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b0e76fec693..b5d0d79e14e 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -46,10 +46,27 @@ describe Event, models: true do
it { expect(@event.author).to eq(@user) }
end
+ describe '#note?' do
+ subject { Event.new(project: target.project, target: target) }
+
+ context 'issue note event' do
+ let(:target) { create(:note_on_issue) }
+
+ it { is_expected.to be_note }
+ end
+
+ context 'merge request diff note event' do
+ let(:target) { create(:legacy_diff_note_on_merge_request) }
+
+ it { is_expected.to be_note }
+ end
+ end
+
describe '#visible_to_user?' do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -61,6 +78,7 @@ describe Event, models: true do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
context 'issue event' do
@@ -71,6 +89,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -81,11 +100,12 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
- context 'note event' do
+ context 'issue note event' do
context 'on non confidential issues' do
let(:target) { note_on_issue }
@@ -93,6 +113,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -103,9 +124,24 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
+
+ context 'merge request diff note event' do
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
+ let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:target) { note_on_merge_request }
+
+ it { expect(event.visible_to_user?(non_member)).to eq true }
+ it { expect(event.visible_to_user?(author)).to eq true }
+ it { expect(event.visible_to_user?(assignee)).to eq true }
+ it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
+ it { expect(event.visible_to_user?(admin)).to eq true }
+ end
end
describe '.limit_recent' do
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 3b817608ce0..fa1a0d4e0c7 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -23,14 +23,12 @@ describe :forked_from_project do
let(:project_from) { create(:project) }
let(:project_to) { create(:project, forked_project_link: forked_project_link) }
-
before :each do
forked_project_link.forked_from_project = project_from
forked_project_link.forked_to_project = project_to
forked_project_link.save!
end
-
it "project_to should know it is forked" do
expect(project_to.forked?).to be_truthy
end
@@ -43,7 +41,6 @@ describe :forked_from_project do
expect(forked_project_link).to receive(:destroy)
project_to.destroy
end
-
end
def fork_project(from_project, user)
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 0caf5869c24..c4e781dd1dc 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline }
+ let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
describe :context do
subject { generic_commit_status.context }
@@ -27,13 +27,13 @@ describe GenericCommitStatus, models: true do
describe :context do
subject { generic_commit_status.context }
- it { is_expected.to_not be_nil }
+ it { is_expected.not_to be_nil }
end
describe :stage do
subject { generic_commit_status.stage }
- it { is_expected.to_not be_nil }
+ it { is_expected.not_to be_nil }
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6fa16be7f04..a878ff1b227 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -5,7 +5,40 @@ describe Group, models: true do
describe 'associations' do
it { is_expected.to have_many :projects }
- it { is_expected.to have_many :group_members }
+ it { is_expected.to have_many(:group_members).dependent(:destroy) }
+ it { is_expected.to have_many(:users).through(:group_members) }
+ it { is_expected.to have_many(:owners).through(:group_members) }
+ it { is_expected.to have_many(:requesters).dependent(:destroy) }
+ it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+ it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
+ it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+
+ describe '#members & #requesters' do
+ let(:requester) { create(:user) }
+ let(:developer) { create(:user) }
+ before do
+ group.request_access(requester)
+ group.add_developer(developer)
+ end
+
+ describe '#members' do
+ it 'includes members and exclude requesters' do
+ member_user_ids = group.members.pluck(:user_id)
+
+ expect(member_user_ids).to include(developer.id)
+ expect(member_user_ids).not_to include(requester.id)
+ end
+ end
+
+ describe '#requesters' do
+ it 'does not include requesters' do
+ requester_user_ids = group.requesters.pluck(:user_id)
+
+ expect(requester_user_ids).to include(requester.id)
+ expect(requester_user_ids).not_to include(developer.id)
+ end
+ end
+ end
end
describe 'modules' do
@@ -131,4 +164,58 @@ describe Group, models: true do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
+
+ describe '#has_owner?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_owner?(@members[:owner])).to be_truthy }
+ it { expect(group.has_owner?(@members[:master])).to be_falsey }
+ it { expect(group.has_owner?(@members[:developer])).to be_falsey }
+ it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_owner?(@members[:guest])).to be_falsey }
+ it { expect(group.has_owner?(@members[:requester])).to be_falsey }
+ end
+
+ describe '#has_master?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_master?(@members[:owner])).to be_falsey }
+ it { expect(group.has_master?(@members[:master])).to be_truthy }
+ it { expect(group.has_master?(@members[:developer])).to be_falsey }
+ it { expect(group.has_master?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_master?(@members[:guest])).to be_falsey }
+ it { expect(group.has_master?(@members[:requester])).to be_falsey }
+ end
+
+ describe '#owners' do
+ let(:owner) { create(:user) }
+ let(:developer) { create(:user) }
+
+ it 'returns the owners of a Group' do
+ group.add_owner(owner)
+ group.add_developer(developer)
+
+ expect(group.owners).to eq([owner])
+ end
+ end
+
+ def setup_group_members(group)
+ members = {
+ owner: create(:user),
+ master: create(:user),
+ developer: create(:user),
+ reporter: create(:user),
+ guest: create(:user),
+ requester: create(:user)
+ }
+
+ group.add_user(members[:owner], GroupMember::OWNER)
+ group.add_user(members[:master], GroupMember::MASTER)
+ group.add_user(members[:developer], GroupMember::DEVELOPER)
+ group.add_user(members[:reporter], GroupMember::REPORTER)
+ group.add_user(members[:guest], GroupMember::GUEST)
+ group.request_access(members[:requester])
+
+ members
+ end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index f800f415bd2..534e1b4f128 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -34,14 +34,14 @@ describe ServiceHook, models: true 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' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
).once
end
it "POSTs the data as JSON" 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' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
).once
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 56a9fbe9720..4078b9e4ff5 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -33,7 +33,7 @@ describe SystemHook, models: true do
Projects::CreateService.new(user, name: 'empty').execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_create/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -42,7 +42,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -51,7 +51,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -60,7 +60,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_destroy/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -69,7 +69,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -79,7 +79,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -88,7 +88,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_create/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -97,7 +97,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_destroy/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -106,7 +106,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_group/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
@@ -116,7 +116,7 @@ describe SystemHook, models: true do
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/,
- headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
+ headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' }
).once
end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 37a27d73aab..f9bab487b96 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -95,13 +95,13 @@ describe WebHook, models: true do
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'])
+ expect(project_hook.execute(@data, 'push_hooks')).to eq([200, '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'])
+ expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success'])
end
end
end
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index 1b987588f59..b3aed66a5b6 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
RSpec.describe Identity, models: true do
-
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 8ab00c70f9d..b87d68283e6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -192,7 +192,7 @@ describe Issue, models: true do
source_project: subject.project,
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
- expect(subject.referenced_merge_requests).to_not be_empty
+ expect(subject.referenced_merge_requests).not_to be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
@@ -231,4 +231,59 @@ describe Issue, models: true do
expect(issue.to_branch_name).to match /confidential-issue\z/
end
end
+
+ describe '#participants' do
+ context 'using a public project' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ let!(:note1) do
+ create(:note_on_issue, noteable: issue, project: project, note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_issue, noteable: issue, project: project, note: 'b')
+ end
+
+ it 'includes the issue author' do
+ expect(issue.participants).to include(issue.author)
+ end
+
+ it 'includes the authors of the notes' do
+ expect(issue.participants).to include(note1.author, note2.author)
+ end
+ end
+
+ context 'using a private project' do
+ it 'does not include mentioned users that do not have access to the project' do
+ project = create(:project)
+ user = create(:user)
+ issue = create(:issue, project: project)
+
+ create(:note_on_issue,
+ noteable: issue,
+ project: project,
+ note: user.to_reference)
+
+ expect(issue.participants).not_to include(user)
+ end
+ end
+ end
+
+ describe 'cached counts' do
+ it 'updates when assignees change' do
+ user1 = create(:user)
+ user2 = create(:user)
+ issue = create(:issue, assignee: user1)
+
+ expect(user1.assigned_open_issues_count).to eq(1)
+ expect(user2.assigned_open_issues_count).to eq(0)
+
+ issue.assignee = user2
+ issue.save
+
+ expect(user1.assigned_open_issues_count).to eq(0)
+ expect(user2.assigned_open_issues_count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb
deleted file mode 100644
index 1634265b439..00000000000
--- a/spec/models/jira_issue_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'spec_helper'
-
-describe JiraIssue do
- let(:project) { create(:project) }
- subject { JiraIssue.new('JIRA-123', project) }
-
- describe 'id' do
- subject { super().id }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'iid' do
- subject { super().iid }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'to_s' do
- subject { super().to_s }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe :== do
- specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) }
- specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) }
-
- it 'only compares with JiraIssues' do
- expect(subject).not_to eq('JIRA-123')
- end
- end
-end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 26fbedbef2f..49cf3d8633a 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -26,7 +26,7 @@ describe Key, models: true do
end
end
- context "validation of uniqueness" do
+ context "validation of uniqueness (based on fingerprint uniqueness)" do
let(:user) { create(:user) }
it "accepts the key once" do
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index dad2628651b..f37f44a608e 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -32,21 +32,20 @@ describe Label, models: true do
it 'should validate title' do
expect(label).not_to allow_value('G,ITLAB').for(:title)
- expect(label).not_to allow_value('G?ITLAB').for(:title)
- expect(label).not_to allow_value('G&ITLAB').for(:title)
expect(label).not_to allow_value('').for(:title)
expect(label).to allow_value('GITLAB').for(:title)
expect(label).to allow_value('gitlab').for(:title)
+ expect(label).to allow_value('G?ITLAB').for(:title)
+ expect(label).to allow_value('G&ITLAB').for(:title)
expect(label).to allow_value("customer's request").for(:title)
end
end
- describe "#title" do
- let(:label) { create(:label, title: "<b>test</b>") }
-
- it "sanitizes title" do
- expect(label.title).to eq("test")
+ describe '#title' do
+ it 'sanitizes title' do
+ label = described_class.new(title: '<b>foo & bar?</b>')
+ expect(label.title).to eq('foo & bar?')
end
end
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
new file mode 100644
index 00000000000..d64d89edbd3
--- /dev/null
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe LegacyDiffNote, models: true do
+ describe "Commit diff line notes" do
+ let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
+ let!(:commit) { note.noteable }
+
+ it "should save a valid note" do
+ expect(note.commit_id).to eq(commit.id)
+ expect(note.noteable.id).to eq(commit.id)
+ end
+
+ it "should be recognized by #legacy_diff_note?" do
+ expect(note).to be_legacy_diff_note
+ end
+ end
+
+ describe '#active?' do
+ it 'is always true when the note has no associated diff' do
+ note = build(:legacy_diff_note_on_merge_request)
+
+ 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(:legacy_diff_note_on_merge_request)
+
+ 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(:legacy_diff_note_on_merge_request)
+
+ 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(:legacy_diff_note_on_merge_request, 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(:legacy_diff_note_on_merge_request, noteable: merge,
+ line_code: code,
+ project: merge.source_project)
+
+ # 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
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6e51730eecd..40181a8b906 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -55,6 +55,64 @@ describe Member, models: true do
end
end
+ describe 'Scopes & finders' do
+ before do
+ project = create(:project)
+ group = create(:group)
+ @owner_user = create(:user).tap { |u| group.add_owner(u) }
+ @owner = group.members.find_by(user_id: @owner_user.id)
+
+ @master_user = create(:user).tap { |u| project.team << [u, :master] }
+ @master = project.members.find_by(user_id: @master_user.id)
+
+ ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+
+ accepted_invite_user = build(:user)
+ ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+
+ requested_user = create(:user).tap { |u| project.request_access(u) }
+ @requested_member = project.requesters.find_by(user_id: requested_user.id)
+
+ accepted_request_user = create(:user).tap { |u| project.request_access(u) }
+ @accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
+ end
+
+ describe '.invite' do
+ it { expect(described_class.invite).not_to include @master }
+ it { expect(described_class.invite).to include @invited_member }
+ it { expect(described_class.invite).not_to include @accepted_invite_member }
+ it { expect(described_class.invite).not_to include @requested_member }
+ it { expect(described_class.invite).not_to include @accepted_request_member }
+ end
+
+ describe '.non_invite' do
+ it { expect(described_class.non_invite).to include @master }
+ it { expect(described_class.non_invite).not_to include @invited_member }
+ it { expect(described_class.non_invite).to include @accepted_invite_member }
+ it { expect(described_class.non_invite).to include @requested_member }
+ it { expect(described_class.non_invite).to include @accepted_request_member }
+ end
+
+ describe '.request' do
+ it { expect(described_class.request).not_to include @master }
+ it { expect(described_class.request).not_to include @invited_member }
+ it { expect(described_class.request).not_to include @accepted_invite_member }
+ it { expect(described_class.request).to include @requested_member }
+ it { expect(described_class.request).not_to include @accepted_request_member }
+ end
+
+ describe '.owners_and_masters' do
+ it { expect(described_class.owners_and_masters).to include @owner }
+ it { expect(described_class.owners_and_masters).to include @master }
+ it { expect(described_class.owners_and_masters).not_to include @invited_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
+ it { expect(described_class.owners_and_masters).not_to include @requested_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+ end
+ end
+
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
@@ -97,6 +155,44 @@ describe Member, models: true do
end
end
+ describe '#accept_request' do
+ let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(member.accept_request).to be_truthy }
+
+ it 'clears requested_at' do
+ member.accept_request
+
+ expect(member.requested_at).to be_nil
+ end
+
+ it 'calls #after_accept_request' do
+ expect(member).to receive(:after_accept_request)
+
+ member.accept_request
+ end
+ end
+
+ describe '#invite?' do
+ subject { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it { is_expected.to be_invite }
+ end
+
+ describe '#request?' do
+ subject { create(:project_member, requested_at: Time.now.utc) }
+
+ it { is_expected.to be_request }
+ end
+
+ describe '#pending?' do
+ let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+ let(:requester) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(invited_member).to be_invite }
+ it { expect(requester).to be_pending }
+ end
+
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5424c9b9cba..18439cac2a4 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
describe GroupMember, models: true do
- context 'notification' do
+ describe 'notifications' do
describe "#after_create" do
it "should send email to user" do
membership = build(:group_member)
@@ -50,5 +50,21 @@ describe GroupMember, models: true do
@group_member.update_attribute(:access_level, GroupMember::OWNER)
end
end
+
+ describe '#after_accept_request' do
+ it 'calls NotificationService.accept_group_access_request' do
+ member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_group_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+
+ describe '#real_source_type' do
+ subject { create(:group_member).real_source_type }
+
+ it { is_expected.to eq 'Group' }
+ end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 9f26d9eb5ce..4c103462433 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -20,6 +20,54 @@
require 'spec_helper'
describe ProjectMember, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to allow_value('Project').for(:source_type) }
+ it { is_expected.not_to allow_value('project').for(:source_type) }
+ end
+
+ describe 'modules' do
+ it { is_expected.to include_module(Gitlab::ShellAdapter) }
+ end
+
+ describe '#real_source_type' do
+ subject { create(:project_member).real_source_type }
+
+ it { is_expected.to eq 'Project' }
+ end
+
+ describe "#destroy" do
+ let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
+ let(:project) { owner.project }
+ let(:master) { create(:project_member, project: project) }
+
+ let(:owner_todos) { (0...2).map { create(:todo, user: owner.user, project: project) } }
+ let(:master_todos) { (0...3).map { create(:todo, user: master.user, project: project) } }
+
+ before do
+ owner_todos
+ master_todos
+ end
+
+ it "destroy itself and delete associated todos" do
+ expect(owner.user.todos.size).to eq(2)
+ expect(master.user.todos.size).to eq(3)
+ expect(Todo.count).to eq(5)
+
+ master_todo_ids = master_todos.map(&:id)
+ master.destroy
+
+ expect(owner.user.todos.size).to eq(2)
+ expect(Todo.count).to eq(2)
+ master_todo_ids.each do |id|
+ expect(Todo.exists?(id)).to eq(false)
+ end
+ end
+ end
+
describe :import_team do
before do
@abilities = Six.new
@@ -71,7 +119,6 @@ describe ProjectMember, models: true do
it { expect(@project_1.users).to include(@user_1) }
it { expect(@project_1.users).to include(@user_2) }
-
it { expect(@project_2.users).to include(@user_1) }
it { expect(@project_2.users).to include(@user_2) }
end
@@ -93,4 +140,16 @@ describe ProjectMember, models: true do
it { expect(@project_1.users).to be_empty }
it { expect(@project_2.users).to be_empty }
end
+
+ describe 'notifications' do
+ describe '#after_accept_request' do
+ it 'calls NotificationService.new_project_member' do
+ member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_project_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c8578749b21..a4b6ff8f8ad 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequest, models: true do
+ include RepoHelpers
+
subject { create(:merge_request) }
describe 'associations' do
@@ -62,37 +64,43 @@ describe MergeRequest, models: true do
end
end
- describe '#target_sha' do
+ describe '#target_branch_sha' do
context 'when the target branch does not exist anymore' do
- subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.repository.raw_repository.delete_branch(subject.target_branch)
+ end
it 'returns nil' do
- expect(subject.target_sha).to be_nil
+ expect(subject.target_branch_sha).to be_nil
end
end
end
- describe '#source_sha' do
+ describe '#source_branch_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)
+ expect(subject.source_branch_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)
+ expect(subject.source_branch_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
+ expect(subject.source_branch_sha).to be_nil
end
end
end
@@ -113,7 +121,8 @@ describe MergeRequest, models: true do
before do
allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
- create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.project)
+ create(:note_on_commit, commit_id: merge_request.commits.first.id,
+ project: merge_request.project)
create(:note, noteable: merge_request, project: merge_request.project)
end
@@ -123,7 +132,9 @@ describe MergeRequest, models: true do
end
it "should include notes for commits from target project as well" do
- create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project)
+ create(:note_on_commit, commit_id: merge_request.commits.first.id,
+ project: merge_request.target_project)
+
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(3)
end
@@ -243,24 +254,31 @@ describe MergeRequest, models: true do
end
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)
+ allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_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
+ subject.source_branch = "lfs"
+
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
end
describe "#reset_merge_when_build_succeeds" do
- let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) }
+ let(:merge_if_green) do
+ create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
+ merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
+ end
it "sets the item to false" do
merge_if_green.reset_merge_when_build_succeeds
merge_if_green.reload
expect(merge_if_green.merge_when_build_succeeds).to be_falsey
+ expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
+ expect(merge_if_green.merge_params["commit_message"]).to be_nil
end
end
@@ -289,7 +307,12 @@ describe MergeRequest, models: true do
let(:fork_project) { create(:project, forked_from_project: project) }
context 'when the target branch does not exist anymore' do
- subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } }
+ subject { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.repository.raw_repository.delete_branch(subject.target_branch)
+ subject.reload
+ end
it 'does not crash' do
expect{ subject.diverged_commits_count }.not_to raise_error
@@ -344,7 +367,7 @@ describe MergeRequest, models: true do
and_return(2)
subject.diverged_commits_count
- allow(subject).to receive(:source_sha).and_return('123abc')
+ allow(subject).to receive(:source_branch_sha).and_return('123abc')
subject.diverged_commits_count
end
@@ -354,7 +377,7 @@ describe MergeRequest, models: true do
and_return(2)
subject.diverged_commits_count
- allow(subject).to receive(:target_sha).and_return('123abc')
+ allow(subject).to receive(:target_branch_sha).and_return('123abc')
subject.diverged_commits_count
end
end
@@ -371,19 +394,18 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
end
- describe '#ci_commit' do
+ describe '#pipeline' do
describe 'when the source project exists' do
- it 'returns the latest commit' do
- commit = double(:commit, id: '123abc')
- ci_commit = double(:ci_commit, ref: 'master')
+ it 'returns the latest pipeline' do
+ pipeline = double(:ci_pipeline, ref: 'master')
- allow(subject).to receive(:last_commit).and_return(commit)
+ allow(subject).to receive(:diff_head_sha).and_return('123abc')
- expect(subject.source_project).to receive(:ci_commit).
+ expect(subject.source_project).to receive(:pipeline).
with('123abc', 'master').
- and_return(ci_commit)
+ and_return(pipeline)
- expect(subject.ci_commit).to eq(ci_commit)
+ expect(subject.pipeline).to eq(pipeline)
end
end
@@ -391,8 +413,240 @@ describe MergeRequest, models: true do
it 'returns nil' do
allow(subject).to receive(:source_project).and_return(nil)
- expect(subject.ci_commit).to be_nil
+ expect(subject.pipeline).to be_nil
end
end
end
+
+ describe '#participants' do
+ let(:project) { create(:project, :public) }
+
+ let(:mr) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ let!(:note1) do
+ create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
+ end
+
+ it 'includes the merge request author' do
+ expect(mr.participants).to include(mr.author)
+ end
+
+ it 'includes the authors of the notes' do
+ expect(mr.participants).to include(note1.author, note2.author)
+ end
+ end
+
+ describe 'cached counts' do
+ it 'updates when assignees change' do
+ user1 = create(:user)
+ user2 = create(:user)
+ mr = create(:merge_request, assignee: user1)
+
+ expect(user1.assigned_open_merge_request_count).to eq(1)
+ expect(user2.assigned_open_merge_request_count).to eq(0)
+
+ mr.assignee = user2
+ mr.save
+
+ expect(user1.assigned_open_merge_request_count).to eq(0)
+ expect(user2.assigned_open_merge_request_count).to eq(1)
+ end
+ end
+
+ describe '#check_if_can_be_merged' do
+ let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }
+
+ subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
+
+ context 'when it is not broken and has no conflicts' do
+ it 'is marked as mergeable' do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(true)
+
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(false)
+ end
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+ end
+
+ describe '#mergeable?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'returns false if #mergeable_state? is false' do
+ expect(subject).to receive(:mergeable_state?) { false }
+
+ expect(subject.mergeable?).to be_falsey
+ end
+
+ it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
+ allow(subject).to receive(:mergeable_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:can_be_merged?) { true }
+
+ expect(subject.mergeable?).to be_truthy
+ end
+ end
+
+ describe '#mergeable_state?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'checks if merge request can be merged' do
+ allow(subject).to receive(:mergeable_ci_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+
+ subject.mergeable?
+ end
+
+ context 'when not open' do
+ before { subject.close }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when working in progress' do
+ before { subject.title = 'WIP MR' }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when failed' do
+ before { allow(subject).to receive(:broken?) { false } }
+
+ context 'when project settings restrict to merge only if build succeeds and build failed' do
+ before do
+ project.only_allow_merge_if_build_succeeds = true
+ allow(subject).to receive(:mergeable_ci_state?) { false }
+ end
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#mergeable_ci_state?' do
+ let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ subject { build(:merge_request, target_project: project) }
+
+ context 'when it is only allowed to merge when build is green' do
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+ end
+
+ context 'when merges are not restricted to green builds' do
+ subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }
+
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+ end
+ end
+
+ describe "#reload_diff" do
+ let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
+
+ let(:commit) { subject.project.commit(sample_commit.id) }
+
+ it "reloads the diff content" do
+ expect(subject.merge_request_diff).to receive(:reload_content)
+
+ subject.reload_diff
+ end
+
+ it "updates diff note positions" do
+ old_diff_refs = subject.diff_refs
+
+ merge_request_diff = subject.merge_request_diff
+
+ # Update merge_request_diff so that #diff_refs will return commit.diff_refs
+ allow(merge_request_diff).to receive(:reload_content) do
+ merge_request_diff.base_commit_sha = commit.parent_id
+ merge_request_diff.start_commit_sha = commit.parent_id
+ merge_request_diff.head_commit_sha = commit.sha
+ end
+
+ expect(Notes::DiffPositionUpdateService).to receive(:new).with(
+ subject.project,
+ nil,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: commit.diff_refs,
+ paths: note.position.paths
+ ).and_call_original
+ expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+
+ expect_any_instance_of(DiffNote).to receive(:save).once
+
+ subject.reload_diff
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 247a9fa9910..1e18c788b50 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -204,4 +204,37 @@ describe Milestone, models: true do
to eq([milestone])
end
end
+
+ describe '.upcoming_ids_by_projects' do
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
+ let(:project_3) { create(:empty_project) }
+ let(:projects) { [project_1, project_2, project_3] }
+
+ let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) }
+ let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) }
+ let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) }
+
+ let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) }
+ let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) }
+ let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) }
+
+ let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) }
+
+ # The call to `#try` is because this returns a relation with a Postgres DB,
+ # and an array of IDs with a MySQL DB.
+ let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } }
+
+ it 'returns the next upcoming open milestone ID for each project' do
+ expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id)
+ end
+
+ context 'when the projects have no open upcoming milestones' do
+ let(:projects) { [project_3] }
+
+ it 'returns no results' do
+ expect(milestone_ids).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4074f966299..5f68cd2b066 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -57,6 +57,7 @@ describe Namespace, models: true do
describe :move_dir do
before do
@namespace = create :namespace
+ @project = create :project, namespace: @namespace
allow(@namespace).to receive(:path_changed?).and_return(true)
end
@@ -70,11 +71,30 @@ describe Namespace, models: true do
allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace.move_dir).to be_truthy
end
+
+ context "when any project has container tags" do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+
+ create(:empty_project, namespace: @namespace)
+
+ allow(@namespace).to receive(:path_was).and_return(@namespace.path)
+ allow(@namespace).to receive(:path).and_return('new_path')
+ end
+
+ it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
+ end
end
describe :rm_dir do
- it "should remove dir" do
- expect(namespace.rm_dir).to be_truthy
+ let!(:project) { create(:project, namespace: namespace) }
+ let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
+
+ before { namespace.destroy }
+
+ it "should remove its dirs when deleted" do
+ expect(File.exist?(path)).to be(false)
end
end
@@ -89,7 +109,6 @@ describe Namespace, models: true do
end
describe ".clean_path" do
-
let!(:user) { create(:user, username: "johngitlab-etc") }
let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 4b788b57882..6549791f675 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -9,9 +9,47 @@ describe Note, models: true do
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
+ describe 'modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(Participable) }
+ it { is_expected.to include_module(Mentionable) }
+ it { is_expected.to include_module(Awardable) }
+
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:note) }
it { is_expected.to validate_presence_of(:project) }
+
+ context 'when note is on commit' do
+ before { allow(subject).to receive(:for_commit?).and_return(true) }
+
+ it { is_expected.to validate_presence_of(:commit_id) }
+ it { is_expected.not_to validate_presence_of(:noteable_id) }
+ end
+
+ context 'when note is not on commit' do
+ before { allow(subject).to receive(:for_commit?).and_return(false) }
+
+ it { is_expected.not_to validate_presence_of(:commit_id) }
+ it { is_expected.to validate_presence_of(:noteable_id) }
+ end
+
+ context 'when noteable and note project differ' do
+ subject do
+ build(:note, noteable: build_stubbed(:issue),
+ project: build_stubbed(:project))
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when noteable and note project are the same' do
+ subject { create(:note) }
+ it { is_expected.to be_valid }
+ end
end
describe "Commit notes" do
@@ -32,23 +70,9 @@ describe Note, models: true do
it "should be recognized by #for_commit?" do
expect(note).to be_for_commit
end
- end
- describe "Commit diff line notes" do
- let!(:note) { create(:note_on_commit_diff, note: "+1 from me") }
- let!(:commit) { note.noteable }
-
- it "should save a valid note" do
- expect(note.commit_id).to eq(commit.id)
- expect(note.noteable.id).to eq(commit.id)
- end
-
- it "should be recognized by #for_diff_line?" do
- expect(note).to be_for_diff_line
- end
-
- it "should be recognized by #for_commit_diff_line?" do
- expect(note).to be_for_commit_diff_line
+ it "keeps the commit around" do
+ expect(note.project.repository.kept_around?(commit.id)).to be_truthy
end
end
@@ -107,12 +131,23 @@ describe Note, models: true do
end
describe "#all_references" do
- let!(:note1) { create(:note) }
- let!(:note2) { create(:note) }
+ let!(:note1) { create(:note_on_issue) }
+ let!(:note2) { create(:note_on_issue) }
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)
+ expect(Banzai::Renderer).to receive(:render).
+ with(note1.note,
+ pipeline: :note,
+ cache_key: [note1, "note"],
+ project: note1.project,
+ author: note1.author)
+
+ expect(Banzai::Renderer).to receive(:render).
+ with(note2.note,
+ pipeline: :note,
+ cache_key: [note2, "note"],
+ project: note2.project,
+ author: note2.author)
note1.all_references
note2.all_references
@@ -120,7 +155,7 @@ describe Note, models: true do
end
describe '.search' do
- let(:note) { create(:note, note: 'WoW') }
+ let(:note) { create(:note_on_issue, note: 'WoW') }
it 'returns notes with matching content' do
expect(described_class.search(note.note)).to eq([note])
@@ -129,81 +164,30 @@ describe Note, models: true do
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
- before do
- create :note, note: "smile", is_award: true
- create :note, note: "smile", is_award: true
- end
-
- it "returns grouped hash of notes" do
- expect(Note.grouped_awards.keys.size).to eq(3)
- expect(Note.grouped_awards["smile"]).to match_array(Note.all)
- end
-
- it "returns thumbsup and thumbsdown always" do
- expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
- expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
- end
- end
-
- describe '#active?' do
- it 'is always true when the note has no associated diff' do
- note = build(:note)
+ context "confidential issues" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
- 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
+ it "returns notes with matching content if user can see the issue" do
+ expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
end
- it '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)
+ it "does not return notes with matching content if user can not see the issue" do
+ user = create(:user)
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
- # We're persisting in order to trigger the set_diff callback
- note = create(:note, noteable: merge, line_code: code)
+ it "does not return notes with matching content for project members with guest role" do
+ user = create(:user)
+ project.team << [user, :guest]
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
- # 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
+ it "does not return notes with matching content for unauthenticated users" do
+ expect(described_class.search(confidential_note.note)).to be_empty
end
end
end
@@ -218,11 +202,6 @@ describe Note, models: true do
note = build(:note, system: true)
expect(note.editable?).to be_falsy
end
-
- it "returns false" do
- note = build(:note, is_award: true, note: "smiley")
- expect(note.editable?).to be_falsy
- end
end
describe "cross_reference_not_visible_for?" do
@@ -249,23 +228,6 @@ describe Note, models: true do
end
end
- describe "set_award!" do
- let(:merge_request) { create :merge_request }
-
- it "converts aliases to actual name" do
- 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: ' ')
@@ -273,4 +235,14 @@ describe Note, models: true do
expect { note.valid? }.to change(note, :line_code).to(nil)
end
end
+
+ describe '#participants' do
+ it 'includes the note author' do
+ project = create(:project, :public)
+ issue = create(:issue, project: project)
+ note = create(:note_on_issue, noteable: issue, project: project)
+
+ expect(note.participants).to include(note.author)
+ end
+ end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 295081e9da1..d58673413c8 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -10,8 +10,49 @@ RSpec.describe NotificationSetting, type: :model do
subject { NotificationSetting.new(source_id: 1, source_type: 'Project') }
it { is_expected.to validate_presence_of(:user) }
- it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:level) }
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) }
+
+ context "events" do
+ let(:user) { create(:user) }
+ let(:notification_setting) { NotificationSetting.new(source_id: 1, source_type: 'Project', user_id: user.id) }
+
+ before do
+ notification_setting.level = "custom"
+ notification_setting.new_note = "true"
+ notification_setting.new_issue = 1
+ notification_setting.close_issue = "1"
+ notification_setting.merge_merge_request = "t"
+ notification_setting.close_merge_request = "nil"
+ notification_setting.reopen_merge_request = "false"
+ notification_setting.save
+ end
+
+ it "parses boolean before saving" do
+ expect(notification_setting.new_note).to eq(true)
+ expect(notification_setting.new_issue).to eq(true)
+ expect(notification_setting.close_issue).to eq(true)
+ expect(notification_setting.merge_merge_request).to eq(true)
+ expect(notification_setting.close_merge_request).to eq(false)
+ expect(notification_setting.reopen_merge_request).to eq(false)
+ end
+ end
+ end
+
+ describe '#for_projects' do
+ let(:user) { create(:user) }
+
+ before do
+ 1.upto(4) do |i|
+ setting = create(:notification_setting, user: user)
+
+ setting.project.update_attributes(pending_delete: true) if i.even?
+ end
+ end
+
+ it 'excludes projects pending delete' do
+ expect(user.notification_settings.for_projects).to all(have_attributes(project: an_instance_of(Project)))
+ expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false))
+ end
end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
new file mode 100644
index 00000000000..46eb71cef14
--- /dev/null
+++ b/spec/models/personal_access_token_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe PersonalAccessToken, models: true do
+ describe ".generate" do
+ it "generates a random token" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token.token).to be_present
+ end
+
+ it "doesn't save the record" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index e771f35811e..9ae461f8c2d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -126,25 +126,25 @@ describe BambooService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a specific URL when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a build URL when bamboo_url has no trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
it 'returns a build URL when bamboo_url has a trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
end
@@ -192,9 +192,9 @@ describe BambooService, models: true do
end
end
- def service(bamboo_url: 'http://gitlab.com')
+ def service(bamboo_url: 'http://gitlab.com/bamboo')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
bamboo_url: bamboo_url,
username: 'mic',
@@ -205,7 +205,7 @@ describe BambooService, models: true do
end
def stub_request(status: 200, body: nil, build_state: 'success')
- bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
new file mode 100644
index 00000000000..a6d9717ccb5
--- /dev/null
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -0,0 +1,49 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+require 'spec_helper'
+
+describe BugzillaService, models: true do
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:project_url) }
+ it { is_expected.to validate_presence_of(:issues_url) }
+ it { is_expected.to validate_presence_of(:new_issue_url) }
+ it_behaves_like 'issue tracker service URL attribute', :project_url
+ it_behaves_like 'issue tracker service URL attribute', :issues_url
+ it_behaves_like 'issue tracker service URL attribute', :new_issue_url
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:project_url) }
+ it { is_expected.not_to validate_presence_of(:issues_url) }
+ it { is_expected.not_to validate_presence_of(:new_issue_url) }
+ end
+ end
+end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 6fb5cad5011..5f618322aab 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -176,86 +176,117 @@ describe HipchatService, models: true do
context "Note events" do
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:snippet) { create(:project_snippet, project: project) }
- let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") }
- let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")}
- let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") }
-
- it "should call Hipchat API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
- hipchat.execute(data)
- expect(WebMock).to have_requested(:post, api_url).once
+ context 'when commit comment event triggered' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user, project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "should call Hipchat API for commit comment events" do
+ data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ hipchat.execute(data)
- message = hipchat.send(:create_message, data)
+ expect(WebMock).to have_requested(:post, api_url).once
- obj_attr = data[:object_attributes]
- commit_id = Commit.truncate_sha(data[:commit][:id])
- title = hipchat.send(:format_title, data[:commit][:message])
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "#{title}" \
- "<pre>a comment on a commit</pre>")
+ obj_attr = data[:object_attributes]
+ commit_id = Commit.truncate_sha(data[:commit][:id])
+ title = hipchat.send(:format_title, data[:commit][:message])
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "#{title}" \
+ "<pre>a comment on a commit</pre>")
+ end
end
- it "should call Hipchat API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
- hipchat.execute(data)
+ context 'when merge request comment event triggered' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project,
+ target_project: project)
+ end
- expect(WebMock).to have_requested(:post, api_url).once
+ let(:merge_request_note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project,
+ note: "merge request note")
+ end
- message = hipchat.send(:create_message, data)
+ it "should call Hipchat API for merge request comment events" do
+ data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ hipchat.execute(data)
- obj_attr = data[:object_attributes]
- merge_id = data[:merge_request]['iid']
- title = data[:merge_request]['title']
+ expect(WebMock).to have_requested(:post, api_url).once
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>merge request note</pre>")
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ merge_id = data[:merge_request]['iid']
+ title = data[:merge_request]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>merge request note</pre>")
+ end
end
- it "should call Hipchat API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
- hipchat.execute(data)
+ context 'when issue comment event triggered' do
+ let(:issue) { create(:issue, project: project) }
+ let(:issue_note) do
+ create(:note_on_issue, noteable: issue, project: project,
+ note: "issue note")
+ end
- message = hipchat.send(:create_message, data)
+ it "should call Hipchat API for issue comment events" do
+ data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ hipchat.execute(data)
- obj_attr = data[:object_attributes]
- issue_id = data[:issue]['iid']
- title = data[:issue]['title']
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>issue note</pre>")
+ obj_attr = data[:object_attributes]
+ issue_id = data[:issue]['iid']
+ title = data[:issue]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>issue note</pre>")
+ end
end
- it "should call Hipchat API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
- hipchat.execute(data)
+ context 'when snippet comment event triggered' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:snippet_note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project,
+ note: "snippet note")
+ end
- expect(WebMock).to have_requested(:post, api_url).once
+ it "should call Hipchat API for snippet comment events" do
+ data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ hipchat.execute(data)
- message = hipchat.send(:create_message, data)
+ expect(WebMock).to have_requested(:post, api_url).once
- obj_attr = data[:object_attributes]
- snippet_id = data[:snippet]['id']
- title = data[:snippet]['title']
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>snippet note</pre>")
+ obj_attr = data[:object_attributes]
+ snippet_id = data[:snippet]['id']
+ title = data[:snippet]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>snippet note</pre>")
+ end
end
end
@@ -303,7 +334,7 @@ describe HipchatService, models: true do
it "should notify only broken" do
hipchat.notify_only_broken_builds = true
hipchat.execute(data)
- expect(WebMock).to_not have_requested(:post, api_url).once
+ expect(WebMock).not_to have_requested(:post, api_url).once
end
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5309cfb99ff..5a97cf370da 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -76,7 +76,8 @@ describe JiraService, models: true do
end
it "should call JIRA API" do
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
@@ -84,7 +85,8 @@ describe JiraService, models: true do
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @api_url).with(
body: /this-is-a-custom-id/
).once
@@ -152,11 +154,9 @@ describe JiraService, models: true do
expect(@jira_service.password).to eq("password")
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
-
end
end
-
describe "Validations" do
context "active" do
before do
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
index 621c83c0cda..7fcfdf0eacd 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -15,7 +15,7 @@ describe SlackService::BuildMessage do
commit: {
status: status,
author_name: 'hacker',
- duration: 10,
+ duration: duration,
},
}
end
@@ -23,9 +23,10 @@ describe SlackService::BuildMessage do
context 'succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
-
+ let(:duration) { 10 }
+
it 'returns a message with information about succeeded build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 second(s)'
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
@@ -35,9 +36,23 @@ describe SlackService::BuildMessage do
context 'failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
+ let(:duration) { 10 }
it 'returns a message with information about failed build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 second(s)'
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ describe '#seconds_name' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 1 }
+
+ it 'returns seconds as singular when there is only one' do
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index f648cbe2dee..0f8889bdf3c 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do
}
end
- let(:color) { '#345' }
+ let(:color) { '#C95823' }
context '#initialize' do
before do
@@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- 'Test User opened <url|issue #100> in <somewhere.com|project_name>: '\
- '*Issue title*')
+ '<somewhere.com|[project_name>] Issue opened by Test User')
expect(subject.attachments).to eq([
{
+ title: "#100 Issue title",
+ title_link: "url",
text: "issue description",
color: color,
}
@@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do
args[:object_attributes][:action] = 'close'
args[:object_attributes][:state] = 'closed'
end
+
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- 'Test User closed <url|issue #100> in <somewhere.com|project_name>: '\
- '*Issue title*')
+ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index d37590cab75..379c3e1219c 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -65,7 +65,7 @@ describe SlackService::NoteMessage, models: true do
expect(message.pretext).to eq("Test User commented on " \
"<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
- expected_attachments = [
+ expected_attachments = [
{
text: "comment on a merge request",
color: color,
@@ -117,7 +117,7 @@ describe SlackService::NoteMessage, models: true do
expect(message.pretext).to eq("Test User commented on " \
"<url|snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*")
- expected_attachments = [
+ expected_attachments = [
{
text: "comment on a snippet",
color: color,
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 6ecab645b49..46dedb66c7c 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -47,7 +47,6 @@ describe SlackService::WikiPageMessage, models: true do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
-
it 'it returns the attachment for a new wiki page' do
expect(subject.attachments).to eq([
{
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index a97b7560137..155f3e74e0d 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -142,13 +142,6 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:snippet) { create(:project_snippet, project: project) }
- let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") }
- let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")}
- let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do
@@ -162,32 +155,61 @@ describe SlackService, models: true do
WebMock.stub_request(:post, webhook_url)
end
- it "should call Slack API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
- slack.execute(data)
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for commit comment events" do
+ data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
- slack.execute(data)
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for merge request comment events" do
+ data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
- slack.execute(data)
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for issue comment events" do
+ data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
- slack.execute(data)
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for snippet comment events" do
+ data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index ad24b895170..474715d24c3 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -126,19 +126,19 @@ describe TeamcityService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has no trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has a trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
end
@@ -180,9 +180,9 @@ describe TeamcityService, models: true do
end
end
- def service(teamcity_url: 'http://gitlab.com')
+ def service(teamcity_url: 'http://gitlab.com/teamcity')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
teamcity_url: teamcity_url,
username: 'mic',
@@ -193,7 +193,7 @@ describe TeamcityService, models: true do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
WebMock.stub_request(:get, teamcity_full_url).to_return(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f6e5b132643..5a27ccbab0a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -11,6 +11,8 @@ describe Project, models: true do
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:milestones).dependent(:destroy) }
it { is_expected.to have_many(:project_members).dependent(:destroy) }
+ it { is_expected.to have_many(:users).through(:project_members) }
+ it { is_expected.to have_many(:requesters).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) }
it { is_expected.to have_many(:deploy_keys_projects).dependent(:destroy) }
@@ -22,13 +24,43 @@ describe Project, models: true do
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:commit_statuses) }
- it { is_expected.to have_many(:ci_commits) }
+ it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:runner_projects) }
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(:environments).dependent(:destroy) }
+ it { is_expected.to have_many(:deployments).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+
+ describe '#members & #requesters' do
+ let(:project) { create(:project) }
+ let(:requester) { create(:user) }
+ let(:developer) { create(:user) }
+ before do
+ project.request_access(requester)
+ project.team << [developer, :developer]
+ end
+
+ describe '#members' do
+ it 'includes members and exclude requesters' do
+ member_user_ids = project.members.pluck(:user_id)
+
+ expect(member_user_ids).to include(developer.id)
+ expect(member_user_ids).not_to include(requester.id)
+ end
+ end
+
+ describe '#requesters' do
+ it 'does not include requesters' do
+ requester_user_ids = project.requesters.pluck(:user_id)
+
+ expect(requester_user_ids).to include(requester.id)
+ expect(requester_user_ids).not_to include(developer.id)
+ end
+ end
+ end
end
describe 'modules' do
@@ -53,14 +85,61 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:path).is_within(0..255) }
it { is_expected.to validate_length_of(:description).is_within(0..2000) }
it { is_expected.to validate_presence_of(:creator) }
- it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) }
it { is_expected.to validate_presence_of(:namespace) }
+ it { is_expected.to validate_presence_of(:repository_storage) }
it 'should not allow new projects beyond user limits' do
project2 = build(:project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
expect(project2).not_to be_valid
- expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/)
+ expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
+ end
+
+ describe 'wiki path conflict' do
+ context "when the new path has been used by the wiki of other Project" do
+ it 'should have an error on the name attribute' do
+ new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
+
+ expect(new_project).not_to be_valid
+ expect(new_project.errors[:name].first).to eq('has already been taken')
+ end
+ end
+
+ context "when the new wiki path has been used by the path of other Project" do
+ it 'should have an error on the name attribute' do
+ project_with_wiki_suffix = create(:project, path: 'foo.wiki')
+ new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
+
+ expect(new_project).not_to be_valid
+ expect(new_project.errors[:name].first).to eq('has already been taken')
+ end
+ end
+ end
+
+ context 'repository storages inclussion' do
+ let(:project2) { build(:project, repository_storage: 'missing') }
+
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ it "should not allow repository storages that don't match a label in the configuration" do
+ expect(project2).not_to be_valid
+ expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
+ end
+ end
+
+ it 'should not allow an invalid URI as import_url' do
+ project2 = build(:project, import_url: 'invalid://')
+
+ expect(project2).not_to be_valid
+ end
+
+ it 'should allow a valid URI as import_url' do
+ project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
+
+ expect(project2).to be_valid
end
end
@@ -90,11 +169,17 @@ describe Project, models: true do
it { is_expected.to respond_to(:repo_exists?) }
it { is_expected.to respond_to(:update_merge_requests) }
it { is_expected.to respond_to(:execute_hooks) }
- it { is_expected.to respond_to(:name_with_namespace) }
it { is_expected.to respond_to(:owner) }
it { is_expected.to respond_to(:path_with_namespace) }
end
+ describe '#name_with_namespace' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
+ it { expect(project.human_name).to eq project.name_with_namespace }
+ end
+
describe '#to_reference' do
let(:project) { create(:empty_project) }
@@ -103,6 +188,24 @@ describe Project, models: true do
end
end
+ describe '#repository_storage_path' do
+ let(:project) { create(:project, repository_storage: 'custom') }
+
+ before do
+ FileUtils.mkdir('tmp/tests/custom_repositories')
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ after do
+ FileUtils.rm_rf('tmp/tests/custom_repositories')
+ end
+
+ it 'returns the repository storage path' do
+ expect(project.repository_storage_path).to eq('tmp/tests/custom_repositories')
+ end
+ end
+
it 'should return valid url to repo' do
project = Project.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
@@ -209,11 +312,11 @@ describe Project, models: true do
it 'should update merge request commits with new one if pushed to source branch' do
project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
merge_request.reload
- expect(merge_request.last_commit.id).to eq(commit_id)
+ expect(merge_request.diff_head_sha).to eq(commit_id)
end
end
- describe :find_with_namespace do
+ describe '.find_with_namespace' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
@@ -224,6 +327,22 @@ describe Project, models: true do
it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
+
+ context 'when multiple projects using a similar name exist' do
+ let(:group) { create(:group, name: 'gitlab') }
+
+ let!(:project1) do
+ create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
+ end
+
+ let!(:project2) do
+ create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
+ end
+
+ it 'returns the row where the path matches literally' do
+ expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
+ end
+ end
end
describe :to_param do
@@ -258,24 +377,66 @@ describe Project, models: true do
end
end
- describe :can_have_issues_tracker_id? do
+ describe :external_issue_tracker do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
- it 'should be true for projects with external issues tracker if issues enabled' do
- expect(ext_project.can_have_issues_tracker_id?).to be_truthy
+ context 'on existing projects with no value for has_external_issue_tracker' do
+ before(:each) do
+ project.update_column(:has_external_issue_tracker, nil)
+ ext_project.update_column(:has_external_issue_tracker, nil)
+ end
+
+ it 'updates the has_external_issue_tracker boolean' do
+ expect do
+ project.external_issue_tracker
+ end.to change { project.reload.has_external_issue_tracker }.to(false)
+
+ expect do
+ ext_project.external_issue_tracker
+ end.to change { ext_project.reload.has_external_issue_tracker }.to(true)
+ end
+ end
+
+ it 'returns nil and does not query services when there is no external issue tracker' do
+ project.build_missing_services
+ project.reload
+
+ expect(project).not_to receive(:services)
+
+ expect(project.external_issue_tracker).to eq(nil)
+ end
+
+ it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
+ ext_project.reload # Factory returns a project with changed attributes
+ ext_project.build_missing_services
+ ext_project.reload
+
+ expect(ext_project).to receive(:services).once.and_call_original
+
+ 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
end
+ end
+
+ describe :cache_has_external_issue_tracker do
+ let(:project) { create(:project) }
+
+ it 'stores true if there is any external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [RedmineService.new])
+ expect(project).to receive(:services).and_return(services)
- it 'should be false for projects with internal issue tracker if issues enabled' do
- expect(project.can_have_issues_tracker_id?).to be_falsey
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(true)
end
- it 'should be always false if issues disabled' do
- project.issues_enabled = false
- ext_project.issues_enabled = false
+ it 'stores false if there is no external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [])
+ expect(project).to receive(:services).and_return(services)
- expect(project.can_have_issues_tracker_id?).to be_falsey
- expect(ext_project.can_have_issues_tracker_id?).to be_falsey
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(false)
end
end
@@ -288,6 +449,14 @@ describe Project, models: true do
it { expect(project.open_branches.map(&:name)).to include('feature') }
it { expect(project.open_branches.map(&:name)).not_to include('master') }
+
+ it "includes branches matching a protected branch wildcard" do
+ expect(project.open_branches.map(&:name)).to include('feature')
+
+ create(:protected_branch, name: 'feat*', project: project)
+
+ expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
+ end
end
describe '#star_count' do
@@ -399,23 +568,23 @@ describe Project, models: true do
end
end
- describe :ci_commit do
+ describe :pipeline do
let(:project) { create :project }
- let(:commit) { create :ci_commit, project: project, ref: 'master' }
+ let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
- subject { project.ci_commit(commit.sha, 'master') }
+ subject { project.pipeline(pipeline.sha, 'master') }
- it { is_expected.to eq(commit) }
+ it { is_expected.to eq(pipeline) }
context 'return latest' do
- let(:commit2) { create :ci_commit, project: project, ref: 'master' }
+ let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
before do
- commit
- commit2
+ pipeline
+ pipeline2
end
- it { is_expected.to eq(commit2) }
+ it { is_expected.to eq(pipeline2) }
end
end
@@ -488,6 +657,21 @@ describe Project, models: true do
end
end
+ context 'repository storage by default' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.repository_storage }
+
+ before do
+ storages = { 'alternative_storage' => '/some/path' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ stub_application_setting(repository_storage: 'alternative_storage')
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(true)
+ end
+
+ it { is_expected.to eq('alternative_storage') }
+ end
+
context 'shared runners by default' do
let(:project) { create(:empty_project) }
@@ -634,21 +818,21 @@ describe Project, models: true 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'])
+ end
+ it 'renames a repository' do
ns = project.namespace_dir
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with("#{ns}/foo", "#{ns}/#{project.path}").
+ with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}").
and_return(true)
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with("#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
+ with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
and_return(true)
expect_any_instance_of(SystemHooksService).
@@ -663,6 +847,17 @@ describe Project, models: true do
project.rename_repo
end
+
+ context 'container registry with tags' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ subject { project.rename_repo }
+
+ it { expect{subject}.to raise_error(Exception) }
+ end
end
describe '#expire_caches_before_rename' do
@@ -729,7 +924,7 @@ describe Project, models: true do
context 'using a regular repository' do
it 'creates the repository' do
expect(shell).to receive(:add_repository).
- with(project.path_with_namespace).
+ with(project.repository_storage_path, project.path_with_namespace).
and_return(true)
expect(project.repository).to receive(:after_create)
@@ -739,7 +934,7 @@ describe Project, models: true do
it 'adds an error if the repository could not be created' do
expect(shell).to receive(:add_repository).
- with(project.path_with_namespace).
+ with(project.repository_storage_path, project.path_with_namespace).
and_return(false)
expect(project.repository).not_to receive(:after_create)
@@ -762,14 +957,175 @@ describe Project, models: true do
describe '#protected_branch?' do
let(:project) { create(:empty_project) }
- it 'returns true when a branch is a protected branch' do
+ it 'returns true when the branch matches a protected branch via direct match' do
project.protected_branches.create!(name: 'foo')
expect(project.protected_branch?('foo')).to eq(true)
end
- it 'returns false when a branch is not a protected branch' do
+ it 'returns true when the branch matches a protected branch via wildcard match' do
+ project.protected_branches.create!(name: 'production/*')
+
+ expect(project.protected_branch?('production/some-branch')).to eq(true)
+ end
+
+ it 'returns false when the branch does not match a protected branch via direct match' do
expect(project.protected_branch?('foo')).to eq(false)
end
+
+ it 'returns false when the branch does not match a protected branch via wildcard match' do
+ project.protected_branches.create!(name: 'production/*')
+
+ expect(project.protected_branch?('staging/some-branch')).to eq(false)
+ end
+ end
+
+ describe "#developers_can_push_to_protected_branch?" do
+ let(:project) { create(:empty_project) }
+
+ context "when the branch matches a protected branch via direct match" do
+ it "returns true if 'Developers can Push' is turned on" do
+ create(:protected_branch, name: "production", project: project, developers_can_push: true)
+
+ expect(project.developers_can_push_to_protected_branch?('production')).to be true
+ end
+
+ it "returns false if 'Developers can Push' is turned off" do
+ create(:protected_branch, name: "production", project: project, developers_can_push: false)
+
+ expect(project.developers_can_push_to_protected_branch?('production')).to be false
+ end
+ end
+
+ context "when the branch matches a protected branch via wilcard match" do
+ it "returns true if 'Developers can Push' is turned on" do
+ create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+
+ expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true
+ end
+
+ it "returns false if 'Developers can Push' is turned off" do
+ create(:protected_branch, name: "production/*", project: project, developers_can_push: false)
+
+ expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false
+ end
+ end
+
+ context "when the branch does not match a protected branch" do
+ it "returns false" do
+ create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+
+ expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false
+ end
+ end
+ end
+
+ describe '#container_registry_path_with_namespace' do
+ let(:project) { create(:empty_project, path: 'PROJECT') }
+
+ subject { project.container_registry_path_with_namespace }
+
+ it { is_expected.not_to eq(project.path_with_namespace) }
+ it { is_expected.to eq(project.path_with_namespace.downcase) }
+ end
+
+ describe '#container_registry_repository' do
+ let(:project) { create(:empty_project) }
+
+ before { stub_container_registry_config(enabled: true) }
+
+ subject { project.container_registry_repository }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ describe '#container_registry_repository_url' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.container_registry_repository_url }
+
+ before { stub_container_registry_config(**registry_settings) }
+
+ context 'for enabled registry' do
+ let(:registry_settings) do
+ {
+ enabled: true,
+ host_port: 'example.com',
+ }
+ end
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context 'for disabled registry' do
+ let(:registry_settings) do
+ {
+ enabled: false
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_container_registry_tags?' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.has_container_registry_tags? }
+
+ context 'for enabled registry' do
+ before { stub_container_registry_config(enabled: true) }
+
+ context 'with tags' do
+ before { stub_container_registry_tags('test', 'test2') }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when no tags' do
+ before { stub_container_registry_tags }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'for disabled registry' do
+ before { stub_container_registry_config(enabled: false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.where_paths_in' do
+ context 'without any paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in([])).to eq([])
+ end
+ end
+
+ context 'without any valid paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in(%w[foo])).to eq([])
+ end
+ end
+
+ context 'with valid paths' do
+ let!(:project1) { create(:project) }
+ let!(:project2) { create(:project) }
+
+ it 'returns the projects matching the paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace,
+ project2.path_with_namespace])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+
+ it 'returns projects regardless of the casing of paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace.upcase,
+ project2.path_with_namespace.upcase])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+ end
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index bacb17a8883..9262aeb6ed8 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -29,6 +29,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::REPORTER)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::REPORTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
@@ -64,50 +67,48 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::MASTER)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::MASTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
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)
+ describe '#find_member' do
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+ let(:requester) { create(:user) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
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
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:requester) { create(:user) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
end
end
@@ -132,4 +133,69 @@ describe ProjectTeam, models: true do
expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
end
+
+ describe '#max_member_access' do
+ let(:requester) { create(:user) }
+
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+
+ context 'when project is not shared with group' do
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+
+ context 'when project is shared with group' do
+ before do
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_master(master)
+ group.add_reporter(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 { expect(project.team.max_member_access(requester.id)).to be_nil }
+
+ context 'but share_with_group_lock is true' do
+ before { project.namespace.update(share_with_group_lock: true) }
+
+ it { expect(project.team.max_member_access(master.id)).to be_nil }
+ it { expect(project.team.max_member_access(reporter.id)).to be_nil }
+ end
+ end
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 532e3f013fd..58b57bd4fef 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -16,6 +16,12 @@ describe ProjectWiki, models: true do
end
end
+ describe '#web_url' do
+ it 'returns the full web URL to the wiki' do
+ expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home")
+ end
+ end
+
describe "#url_to_repo" do
it "returns the correct ssh url to the repo" do
expect(subject.url_to_repo).to eq(gitlab_shell.url_to_repo(subject.path_with_namespace))
@@ -38,7 +44,8 @@ describe ProjectWiki, models: true do
describe "#wiki_base_path" do
it "returns the wiki base path" do
- wiki_base_path = "/#{project.path_with_namespace}/wikis"
+ wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.path_with_namespace}/wikis"
+
expect(subject.wiki_base_path).to eq(wiki_base_path)
end
end
@@ -256,6 +263,13 @@ describe ProjectWiki, models: true do
end
end
+ describe '#hook_attrs' do
+ it 'returns a hash with values' do
+ expect(subject.hook_attrs).to be_a Hash
+ expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
+ end
+ end
+
private
def create_temp_repo(path)
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index b523834c6e9..8bf0d24a128 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ProtectedBranch, models: true do
+ subject { build_stubbed(:protected_branch) }
+
describe 'Associations' do
it { is_expected.to belong_to(:project) }
end
@@ -12,4 +14,127 @@ describe ProtectedBranch, models: true do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
end
+
+ describe "#matches?" do
+ context "when the protected branch setting is not a wildcard" do
+ let(:protected_branch) { build(:protected_branch, name: "production/some-branch") }
+
+ it "returns true for branch names that are an exact match" do
+ expect(protected_branch.matches?("production/some-branch")).to be true
+ end
+
+ it "returns false for branch names that are not an exact match" do
+ expect(protected_branch.matches?("staging/some-branch")).to be false
+ end
+ end
+
+ context "when the protected branch name contains wildcard(s)" do
+ context "when there is a single '*'" do
+ let(:protected_branch) { build(:protected_branch, name: "production/*") }
+
+ it "returns true for branch names matching the wildcard" do
+ expect(protected_branch.matches?("production/some-branch")).to be true
+ expect(protected_branch.matches?("production/")).to be true
+ end
+
+ it "returns false for branch names not matching the wildcard" do
+ expect(protected_branch.matches?("staging/some-branch")).to be false
+ expect(protected_branch.matches?("production")).to be false
+ end
+ end
+
+ context "when the wildcard contains regex symbols other than a '*'" do
+ let(:protected_branch) { build(:protected_branch, name: "pro.duc.tion/*") }
+
+ it "returns true for branch names matching the wildcard" do
+ expect(protected_branch.matches?("pro.duc.tion/some-branch")).to be true
+ end
+
+ it "returns false for branch names not matching the wildcard" do
+ expect(protected_branch.matches?("production/some-branch")).to be false
+ expect(protected_branch.matches?("proXducYtion/some-branch")).to be false
+ end
+ end
+
+ context "when there are '*'s at either end" do
+ let(:protected_branch) { build(:protected_branch, name: "*/production/*") }
+
+ it "returns true for branch names matching the wildcard" do
+ expect(protected_branch.matches?("gitlab/production/some-branch")).to be true
+ expect(protected_branch.matches?("/production/some-branch")).to be true
+ expect(protected_branch.matches?("gitlab/production/")).to be true
+ expect(protected_branch.matches?("/production/")).to be true
+ end
+
+ it "returns false for branch names not matching the wildcard" do
+ expect(protected_branch.matches?("gitlabproductionsome-branch")).to be false
+ expect(protected_branch.matches?("production/some-branch")).to be false
+ expect(protected_branch.matches?("gitlab/production")).to be false
+ expect(protected_branch.matches?("production")).to be false
+ end
+ end
+
+ context "when there are arbitrarily placed '*'s" do
+ let(:protected_branch) { build(:protected_branch, name: "pro*duction/*/gitlab/*") }
+
+ it "returns true for branch names matching the wildcard" do
+ expect(protected_branch.matches?("production/some-branch/gitlab/second-branch")).to be true
+ expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/second-branch")).to be true
+ expect(protected_branch.matches?("proXYZduction/gitlab/gitlab/gitlab")).to be true
+ expect(protected_branch.matches?("proXYZduction//gitlab/")).to be true
+ expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/")).to be true
+ expect(protected_branch.matches?("proXYZduction//gitlab/some-branch")).to be true
+ end
+
+ it "returns false for branch names not matching the wildcard" do
+ expect(protected_branch.matches?("production/some-branch/not-gitlab/second-branch")).to be false
+ expect(protected_branch.matches?("prodXYZuction/some-branch/gitlab/second-branch")).to be false
+ expect(protected_branch.matches?("proXYZduction/gitlab/some-branch/gitlab")).to be false
+ expect(protected_branch.matches?("proXYZduction/gitlab//")).to be false
+ expect(protected_branch.matches?("proXYZduction/gitlab/")).to be false
+ expect(protected_branch.matches?("proXYZduction//some-branch/gitlab")).to be false
+ end
+ end
+ end
+ end
+
+ describe "#matching" do
+ context "for direct matches" do
+ it "returns a list of protected branches matching the given branch name" do
+ production = create(:protected_branch, name: "production")
+ staging = create(:protected_branch, name: "staging")
+
+ expect(ProtectedBranch.matching("production")).to include(production)
+ expect(ProtectedBranch.matching("production")).not_to include(staging)
+ end
+
+ it "accepts a list of protected branches to search from, so as to avoid a DB call" do
+ production = build(:protected_branch, name: "production")
+ staging = build(:protected_branch, name: "staging")
+
+ expect(ProtectedBranch.matching("production")).to be_empty
+ expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging)
+ end
+ end
+
+ context "for wildcard matches" do
+ it "returns a list of protected branches matching the given branch name" do
+ production = create(:protected_branch, name: "production/*")
+ staging = create(:protected_branch, name: "staging/*")
+
+ expect(ProtectedBranch.matching("production/some-branch")).to include(production)
+ expect(ProtectedBranch.matching("production/some-branch")).not_to include(staging)
+ end
+
+ it "accepts a list of protected branches to search from, so as to avoid a DB call" do
+ production = build(:protected_branch, name: "production/*")
+ staging = build(:protected_branch, name: "staging/*")
+
+ expect(ProtectedBranch.matching("production/some-branch")).to be_empty
+ expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging)
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 34a13f9b5c9..24e49c8def3 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -12,8 +12,8 @@ describe Repository, models: true do
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)
+ merge_commit_sha = repository.merge(user, source_sha, 'master', commit_options)
+ repository.commit(merge_commit_sha)
end
describe :branch_names_contains do
@@ -31,6 +31,47 @@ describe Repository, models: true do
it { is_expected.not_to include('v1.0.0') }
end
+ describe 'tags_sorted_by' do
+ context 'name' do
+ subject { repository.tags_sorted_by('name').map(&:name) }
+
+ it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
+ end
+
+ context 'updated' do
+ let(:tag_a) { repository.find_tag('v1.0.0') }
+ let(:tag_b) { repository.find_tag('v1.1.0') }
+
+ context 'desc' do
+ subject { repository.tags_sorted_by('updated_desc').map(&:name) }
+
+ before do
+ double_first = double(committed_date: Time.now)
+ double_last = double(committed_date: Time.now - 1.second)
+
+ allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first)
+ allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last)
+ end
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ end
+
+ context 'asc' do
+ subject { repository.tags_sorted_by('updated_asc').map(&:name) }
+
+ before do
+ double_first = double(committed_date: Time.now - 1.second)
+ double_last = double(committed_date: Time.now)
+
+ allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last)
+ allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first)
+ end
+
+ it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
+ end
+ end
+ end
+
describe :last_commit_for_path do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
@@ -100,6 +141,12 @@ describe Repository, models: true do
expect(results.first).not_to start_with('fatal:')
end
+ it 'properly handles an unmatched parenthesis' do
+ results = repository.search_files("test(", 'master')
+
+ expect(results.first).not_to start_with('fatal:')
+ end
+
describe 'result' do
subject { results.first }
@@ -176,6 +223,15 @@ describe Repository, models: true do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
+ it 'handles when HEAD points to non-existent ref' do
+ repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ rugged = double('rugged')
+ expect(rugged).to receive(:head_unborn?).and_return(true)
+ expect(repository).to receive(:rugged).and_return(rugged)
+
+ expect(repository.license_blob).to be_nil
+ end
+
it 'looks in the root_ref only' do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
@@ -204,6 +260,15 @@ describe Repository, models: true do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
+ it 'handles when HEAD points to non-existent ref' do
+ repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ rugged = double('rugged')
+ expect(rugged).to receive(:head_unborn?).and_return(true)
+ expect(repository).to receive(:rugged).and_return(rugged)
+
+ expect(repository.license_key).to be_nil
+ end
+
it 'returns nil when no license is detected' do
expect(repository.license_key).to be_nil
end
@@ -243,14 +308,14 @@ describe Repository, models: true do
describe :add_branch do
context 'when pre hooks were successful' do
it 'should run without errors' do
- hook = double(trigger: true)
+ hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
end
it 'should create the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
branch = repository.add_branch(user, 'new_feature', 'master')
@@ -266,7 +331,7 @@ describe Repository, models: true do
context 'when pre hooks failed' do
it 'should get an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.add_branch(user, 'new_feature', 'master')
@@ -274,7 +339,7 @@ describe Repository, models: true do
end
it 'should not create the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.add_branch(user, 'new_feature', 'master')
@@ -287,13 +352,13 @@ describe Repository, models: true do
describe :rm_branch do
context 'when pre hooks were successful' do
it 'should run without errors' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
it 'should delete the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
@@ -303,7 +368,7 @@ describe Repository, models: true do
context 'when pre hooks failed' do
it 'should get an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'new_feature')
@@ -311,7 +376,7 @@ describe Repository, models: true do
end
it 'should not delete the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'feature')
@@ -343,7 +408,7 @@ describe Repository, models: true do
context 'when pre hooks failed' do
it 'should get an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
@@ -419,7 +484,7 @@ describe Repository, models: true do
end
it 'does nothing' do
- expect(repository.raw_repository).to_not receive(:autocrlf=).
+ expect(repository.raw_repository).not_to receive(:autocrlf=).
with(:input)
repository.update_autocrlf_option
@@ -466,8 +531,6 @@ describe Repository, models: true do
describe '#expire_cache' do
it 'expires all caches' do
expect(repository).to receive(:expire_branch_cache)
- expect(repository).to receive(:expire_branch_count_cache)
- expect(repository).to receive(:expire_tag_count_cache)
repository.expire_cache
end
@@ -487,7 +550,7 @@ describe Repository, models: true do
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)
+ expect(repository).not_to receive(:expire_emptiness_caches)
repository.expire_cache
end
@@ -650,7 +713,7 @@ describe Repository, models: true do
end
it 'does not flush caches that depend on repository data' do
- expect(repository).to_not receive(:expire_cache)
+ expect(repository).not_to receive(:expire_cache)
repository.before_delete
end
@@ -792,7 +855,6 @@ describe Repository, models: true do
repository.after_create
end
-
end
describe "#copy_gitattributes" do
@@ -805,18 +867,6 @@ describe Repository, models: true do
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)
@@ -927,7 +977,7 @@ describe Repository, models: true do
expect(repository.avatar).to eq('logo.png')
- expect(repository).to_not receive(:blob_at_branch)
+ expect(repository).not_to receive(:blob_at_branch)
expect(repository.avatar).to eq('logo.png')
end
end
@@ -1002,12 +1052,14 @@ describe Repository, models: true do
let(:cache) { repository.send(:cache) }
it 'builds the caches if they do not already exist' do
+ cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
+
expect(cache).to receive(:exist?).
- exactly(repository.cache_keys.length).
+ exactly(cache_keys.length).
times.
and_return(false)
- repository.cache_keys.each do |key|
+ cache_keys.each do |key|
expect(repository).to receive(key)
end
@@ -1015,13 +1067,15 @@ describe Repository, models: true do
end
it 'does not build any caches that already exist' do
+ cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags
+
expect(cache).to receive(:exist?).
- exactly(repository.cache_keys.length).
+ exactly(cache_keys.length).
times.
and_return(true)
- repository.cache_keys.each do |key|
- expect(repository).to_not receive(key)
+ cache_keys.each do |key|
+ expect(repository).not_to receive(key)
end
repository.build_cache
@@ -1063,6 +1117,14 @@ describe Repository, models: true do
end
end
+ describe "#keep_around" do
+ it "stores a reference to the specified commit sha so it isn't garbage collected" do
+ repository.keep_around(sample_commit.id)
+
+ expect(repository.kept_around?(sample_commit.id)).to be_truthy
+ end
+ end
+
def create_remote_branch(remote_name, branch_name, target)
rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 8592e112c50..96bbbec9ea1 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe Service, models: true do
-
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -176,7 +175,6 @@ describe Service, models: true do
)
end
-
it "returns nil when the property has not been assigned a new value" do
service.username = "key_changed"
expect(service.bamboo_url_was).to be_nil
@@ -204,4 +202,37 @@ describe Service, models: true do
expect(service.bamboo_url_was).to be_nil
end
end
+
+ describe "callbacks" do
+ let(:project) { create(:project) }
+ let!(:service) do
+ RedmineService.new(
+ project: project,
+ active: true,
+ properties: {
+ project_url: 'http://redmine/projects/project_name_in_redmine',
+ issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id",
+ new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new'
+ }
+ )
+ end
+
+ describe "on create" do
+ it "updates the has_external_issue_tracker boolean" do
+ expect do
+ service.save!
+ end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+ end
+ end
+
+ describe "on update" do
+ it "updates the has_external_issue_tracker boolean" do
+ service.save!
+
+ expect do
+ service.update_attributes(active: false)
+ end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
+ end
+ end
+ end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 7a613e360d4..0621c6a06ce 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -72,7 +72,7 @@ describe Snippet, models: true do
end
end
- describe '#search_code' do
+ describe '.search_code' do
let(:snippet) { create(:snippet, content: 'class Foo; end') }
it 'returns snippets with matching content' do
@@ -87,4 +87,71 @@ describe Snippet, models: true do
expect(described_class.search_code('FOO')).to eq([snippet])
end
end
+
+ describe '.accessible_to' do
+ let(:author) { create(:author) }
+ let(:project) { create(:empty_project) }
+
+ let!(:public_snippet) { create(:snippet, :public) }
+ let!(:internal_snippet) { create(:snippet, :internal) }
+ let!(:private_snippet) { create(:snippet, :private, author: author) }
+
+ let!(:project_public_snippet) { create(:snippet, :public, project: project) }
+ let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
+ let!(:project_private_snippet) { create(:snippet, :private, project: project) }
+
+ it 'returns only public snippets when user is blank' do
+ expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
+ end
+
+ it 'returns only public, and internal snippets for regular users' do
+ user = create(:user)
+
+ expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
+ end
+
+ it 'returns public, internal snippets and project private snippets for project members' do
+ member = create(:user)
+ project.team << [member, :developer]
+
+ expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ end
+
+ it 'returns private snippets where the user is the author' do
+ expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
+ end
+
+ it 'returns all snippets when for admins' do
+ admin = create(:admin)
+
+ expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ end
+ end
+
+ describe '#participants' do
+ let(:project) { create(:project, :public) }
+ let(:snippet) { create(:snippet, content: 'foo', project: project) }
+
+ let!(:note1) do
+ create(:note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'b')
+ end
+
+ it 'includes the snippet author' do
+ expect(snippet.participants).to include(snippet.author)
+ end
+
+ it 'includes the note authors' do
+ expect(snippet.participants).to include(note1.author, note2.author)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 26d4e139396..3984b30ddf8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -30,6 +30,27 @@ describe User, models: true do
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) }
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+
+ describe '#group_members' do
+ it 'does not include group memberships for which user is a requester' do
+ user = create(:user)
+ group = create(:group, :public)
+ group.request_access(user)
+
+ expect(user.group_members).to be_empty
+ end
+ end
+
+ describe '#project_members' do
+ it 'does not include project memberships for which user is a requester' do
+ user = create(:user)
+ project = create(:project, :public)
+ project.request_access(user)
+
+ expect(user.project_members).to be_empty
+ end
+ end
end
describe 'validations' do
@@ -67,7 +88,10 @@ describe User, models: true do
describe 'email' do
context 'when no signup domains listed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
+ end
+
it 'accepts any email' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
@@ -75,7 +99,10 @@ describe User, models: true do
end
context 'when a signup domain is listed and subdomains are allowed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
+ end
+
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
@@ -93,7 +120,9 @@ describe User, models: true do
end
context 'when a signup domain is listed and subdomains are not allowed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com']) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
+ end
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
@@ -120,6 +149,66 @@ describe User, models: true do
end
end
+ describe "scopes" do
+ describe ".with_two_factor" do
+ it "returns users with 2fa enabled via OTP" do
+ user_with_2fa = create(:user, :two_factor_via_otp)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to include(user_with_2fa.id)
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+
+ it "returns users with 2fa enabled via U2F" do
+ user_with_2fa = create(:user, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to include(user_with_2fa.id)
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+
+ it "returns users with 2fa enabled via OTP and U2F" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to eq([user_with_2fa.id])
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+ end
+
+ describe ".without_two_factor" do
+ it "excludes users with 2fa enabled via OTP" do
+ user_with_2fa = create(:user, :two_factor_via_otp)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via U2F" do
+ user_with_2fa = create(:user, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via OTP and U2F" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+ end
+ end
+
describe "Respond to" do
it { is_expected.to respond_to(:is_admin?) }
it { is_expected.to respond_to(:name) }
@@ -141,6 +230,10 @@ describe User, models: true do
end
describe '#confirm' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
+ end
+
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do
@@ -353,6 +446,7 @@ describe User, models: true do
it { expect(user.can_create_group?).to be_truthy }
it { expect(user.can_create_project?).to be_truthy }
it { expect(user.first_name).to eq('John') }
+ it { expect(user.external).to be_falsey }
end
describe 'with defaults' do
@@ -375,6 +469,26 @@ describe User, models: true do
expect(user.theme_id).to eq(1)
end
end
+
+ context 'when current_application_settings.user_default_external is true' do
+ before do
+ stub_application_setting(user_default_external: true)
+ end
+
+ it "creates external user by default" do
+ user = build(:user)
+
+ expect(user.external).to be_truthy
+ end
+
+ describe 'with default overrides' do
+ it "creates a non-external user" do
+ user = build(:user, external: false)
+
+ expect(user.external).to be_falsey
+ end
+ end
+ end
end
describe '.find_by_any_email' do
@@ -782,4 +896,92 @@ describe User, models: true do
it { is_expected.to eq([private_project]) }
end
+
+ describe '#ci_authorized_runners' do
+ let(:user) { create(:user) }
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ project.runners << runner
+ end
+
+ context 'without any projects' do
+ let(:project) { create(:project) }
+
+ it 'does not load' do
+ expect(user.ci_authorized_runners).to be_empty
+ end
+ end
+
+ context 'with personal projects runners' do
+ let(:namespace) { create(:namespace, owner: user) }
+ let(:project) { create(:project, namespace: namespace) }
+
+ it 'loads' do
+ expect(user.ci_authorized_runners).to contain_exactly(runner)
+ end
+ end
+
+ shared_examples :member do
+ context 'when the user is a master' do
+ before do
+ add_user(Gitlab::Access::MASTER)
+ end
+
+ it 'loads' do
+ expect(user.ci_authorized_runners).to contain_exactly(runner)
+ end
+ end
+
+ context 'when the user is a developer' do
+ before do
+ add_user(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'does not load' do
+ expect(user.ci_authorized_runners).to be_empty
+ end
+ end
+ end
+
+ context 'with groups projects runners' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ def add_user(access)
+ group.add_user(user, access)
+ end
+
+ it_behaves_like :member
+ end
+
+ context 'with other projects runners' do
+ let(:project) { create(:project) }
+
+ def add_user(access)
+ project.team << [user, access]
+ end
+
+ it_behaves_like :member
+ end
+ end
+
+ describe '#viewable_starred_projects' do
+ let(:user) { create(:user) }
+ let(:public_project) { create(:empty_project, :public) }
+ let(:private_project) { create(:empty_project, :private) }
+ let(:private_viewable_project) { create(:empty_project, :private) }
+
+ before do
+ private_viewable_project.team << [user, Gitlab::Access::MASTER]
+
+ [public_project, private_project, private_viewable_project].each do |project|
+ user.toggle_star(project)
+ end
+ end
+
+ it 'returns only starred projects the user can view' do
+ expect(user.viewable_starred_projects).not_to include(private_project)
+ end
+ end
end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0c19094ec54..83025953889 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
-describe API, api: true do
+describe API::Helpers, api: true do
include API::Helpers
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -39,24 +40,64 @@ describe API, api: true do
end
describe ".current_user" do
- it "should return nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "should return nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ describe "when authenticating using a user's private token" do
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ expect(current_user).to eq(user)
+ end
end
- it "should leave user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
- expect(current_user).to eq(user)
+ describe "when authenticating using a user's personal access tokens" do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+ expect(current_user).to eq(user)
+ end
+
+ it 'does not allow revoked tokens' do
+ personal_access_token.revoke!
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it 'does not allow expired tokens' do
+ personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
end
it "should change current user to sudo when admin" do
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
new file mode 100644
index 00000000000..72a6d45f47d
--- /dev/null
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ let!(:note) { create(:note, project: project, noteable: issue) }
+
+ before { project.team << [user, :master] }
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
+ context 'on an issue' do
+ it "returns an array of award_emoji" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award_emoji.name)
+ end
+
+ it "should return a 404 error when issue id not found" do
+ get api("/projects/#{project.id}/issues/12345/award_emoji", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it "returns an array of award_emoji" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(downvote.name)
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an array of award emoji' do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(rocket.name)
+ end
+ end
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
+ context 'on an issue' do
+ it "returns the award emoji" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award_emoji.name)
+ expect(json_response['awardable_id']).to eq(issue.id)
+ expect(json_response['awardable_type']).to eq("Issue")
+ end
+
+ it "returns a 404 error if the award is not found" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it 'returns the award emoji' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(downvote.name)
+ expect(json_response['awardable_id']).to eq(merge_request.id)
+ expect(json_response['awardable_type']).to eq("MergeRequest")
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an award emoji' do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).not_to be_an Array
+ expect(json_response['name']).to eq(rocket.name)
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+ context "on an issue" do
+ it "creates a new award emoji" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+
+ it "should return a 400 bad request error if the name is not given" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "should return a 401 unauthorized error if the user is not authenticated" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+ it 'creates a new award emoji' do
+ expect do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ end.to change { note.award_emoji.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
+ context 'when the awardable is an Issue' do
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 error when the award emoji can not be found' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the awardable is a Merge Request' do
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+ end.to change { note.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 55582aa53d2..b11ca26ee68 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
project.repository.expire_cache
get api("/projects/#{project.id}/repository/branches", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
@@ -27,7 +27,7 @@ describe API::API, api: true do
describe "GET /projects/:id/repository/branches/:branch" do
it "should return the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
@@ -36,19 +36,19 @@ describe API::API, api: true do
it "should return a 403 error if guest" do
get api("/projects/#{project.id}/repository/branches", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should return a 404 error if branch is not available" do
get api("/projects/#{project.id}/repository/branches/unknown", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe "PUT /projects/:id/repository/branches/:branch/protect" do
it "should protect a single branch" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
@@ -57,25 +57,25 @@ describe API::API, api: true do
it "should return a 404 error if branch not found" do
put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return a 403 error if guest" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should return success when protect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
it "should unprotect a single branch" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
@@ -84,13 +84,13 @@ describe API::API, api: true do
it "should return success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return success when unprotect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -100,7 +100,7 @@ describe API::API, api: true do
branch_name: 'feature1',
ref: branch_sha
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq('feature1')
expect(json_response['commit']['id']).to eq(branch_sha)
@@ -110,14 +110,14 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/branches", user2),
branch_name: branch_name,
ref: branch_sha
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should return 400 if branch name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new design',
ref: branch_sha
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch name is invalid')
end
@@ -125,12 +125,12 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch already exists')
end
@@ -138,7 +138,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design3',
ref: 'foo'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Invalid reference name')
end
end
@@ -150,25 +150,25 @@ describe API::API, api: true do
it "should remove branch" do
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['branch_name']).to eq(branch_name)
end
it 'should return 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should remove protected branch" do
project.protected_branches.create(name: branch_name)
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Protected branch cant be removed')
end
it "should not remove HEAD branch" do
delete api("/projects/#{project.id}/repository/branches/master", user)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Cannot remove HEAD branch')
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 5ead735be48..f5b39c3d698 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,8 +9,8 @@ describe API::API, api: true do
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) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
@@ -19,15 +19,20 @@ describe API::API, api: true do
context 'authorized user' do
it 'should return project builds' do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
context 'filter project with one scope element' do
let(:query) { 'scope=pending' }
it do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -36,7 +41,7 @@ describe API::API, api: true do
let(:query) { 'scope[0]=pending&scope[1]=running' }
it do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -44,7 +49,7 @@ describe API::API, api: true do
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) }
+ it { expect(response).to have_http_status(400) }
end
end
@@ -52,29 +57,66 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not return project builds' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
describe 'GET /projects/:id/repository/commits/:sha/builds' do
- before do
- project.ensure_ci_commit(commit.sha, 'master')
- get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user)
- end
+ context 'when commit does not exist in repository' do
+ before do
+ get api("/projects/#{project.id}/repository/commits/1a271fd1/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
+ it 'responds with 404' do
+ expect(response).to have_http_status(404)
end
end
- context 'unauthorized user' do
- let(:api_user) { nil }
+ context 'when commit exists in repository' do
+ context 'when user is authorized' do
+ context 'when pipeline has builds' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+ create(:ci_build)
+
+ get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
+ end
+
+ it 'should return project builds for specific commit' do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq 2
+ end
+ end
- it 'should not return project builds' do
- expect(response.status).to eq(401)
+ context 'when pipeline has no builds' do
+ before do
+ branch_head = project.commit('feature').id
+ get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
+ end
+
+ it 'returns an empty array' do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when user is not authorized' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+
+ get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
+ end
+
+ it 'should not return project builds' do
+ expect(response).to have_http_status(401)
+ expect(json_response.except('message')).to be_empty
+ end
end
end
end
@@ -84,7 +126,7 @@ describe API::API, api: true do
context 'authorized user' do
it 'should return specific build data' do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
end
@@ -93,7 +135,7 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not return specific build data' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -102,16 +144,16 @@ describe API::API, api: true 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) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
context 'authorized user' do
let(:download_headers) do
- { 'Content-Transfer-Encoding'=>'binary',
- 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' }
+ { '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).to have_http_status(200)
expect(response.headers).to include(download_headers)
end
end
@@ -120,24 +162,24 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not return specific build artifacts' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
it 'should not return build artifacts if not uploaded' do
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/builds/:build_id/trace' do
- let(:build) { create(:ci_build, :trace, commit: commit) }
-
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
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).to have_http_status(200)
expect(response.body).to eq(build.trace)
end
end
@@ -146,7 +188,7 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not return specific build trace' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -157,7 +199,7 @@ describe API::API, api: true do
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(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
end
@@ -166,7 +208,7 @@ describe API::API, api: true do
let(:api_user) { user2 }
it 'should not cancel build' do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -175,20 +217,20 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not cancel build' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
describe 'POST /projects/:id/builds/:build_id/retry' do
- let(:build) { create(:ci_build, :canceled, commit: commit) }
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
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(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
end
@@ -198,7 +240,7 @@ describe API::API, api: true do
let(:api_user) { user2 }
it 'should not retry build' do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -207,7 +249,7 @@ describe API::API, api: true do
let(:api_user) { nil }
it 'should not retry build' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -218,7 +260,7 @@ describe API::API, api: true do
end
context 'build is erasable' do
- let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) }
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
it 'should erase build content' do
expect(response.status).to eq 201
@@ -234,11 +276,37 @@ describe API::API, api: true do
end
context 'build is not erasable' do
- let(:build) { create(:ci_build, :trace, project: project, commit: commit) }
+ let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
it 'should respond with forbidden' do
expect(response.status).to eq 403
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response.status).to eq 200
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 633927c8c3e..2da01da7fa1 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -5,19 +5,18 @@ describe API::CommitStatuses, api: true do
let!(:project) { create(:project) }
let(:commit) { project.repository.commit }
- let(:commit_status) { create(:commit_status, commit: ci_commit) }
+ let(:commit_status) { create(:commit_status, pipeline: pipeline) }
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
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
- let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') }
- let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') }
+ let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') }
+ let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') }
it_behaves_like 'a paginated resources' do
let(:request) { get api(get_url, reporter) }
@@ -27,7 +26,7 @@ describe API::CommitStatuses, api: true do
let(:statuses_id) { json_response.map { |status| status['id'] } }
def create_status(commit, opts = {})
- create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts))
+ create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts))
end
let!(:status1) { create_status(master, status: 'running') }
@@ -41,7 +40,7 @@ describe API::CommitStatuses, api: true do
before { get api(get_url, reporter) }
it 'returns latest commit statuses' do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
@@ -54,7 +53,7 @@ describe API::CommitStatuses, api: true do
before { get api(get_url, reporter), all: 1 }
it 'returns all commit statuses' do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status1.id, status2.id,
@@ -67,7 +66,7 @@ describe API::CommitStatuses, api: true 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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status5.id)
@@ -78,7 +77,7 @@ describe API::CommitStatuses, api: true 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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status4.id, status5.id)
@@ -101,7 +100,7 @@ describe API::CommitStatuses, api: true do
before { get api(get_url, guest) }
it "should not return project commits" do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -109,7 +108,7 @@ describe API::CommitStatuses, api: true do
before { get api(get_url) }
it "should not return project commits" do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -122,7 +121,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, developer), state: 'success' }
it 'creates commit status' do
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('default')
@@ -141,7 +140,7 @@ describe API::CommitStatuses, api: true do
end
it 'creates commit status' do
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('coverage')
@@ -155,7 +154,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, developer), state: 'invalid' }
it 'does not create commit status' do
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -163,7 +162,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, developer) }
it 'does not create commit status' do
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -172,7 +171,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, developer), state: 'running' }
it 'returns not found error' do
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -181,7 +180,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, reporter) }
it 'should not create commit status' do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -189,7 +188,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url, guest) }
it 'should not create commit status' do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -197,7 +196,7 @@ describe API::CommitStatuses, api: true do
before { post api(post_url) }
it 'should not create commit status' do
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index cb82ca7802d..5219c808791 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -19,7 +19,7 @@ describe API::API, api: true do
it "should return project commits" do
get api("/projects/#{project.id}/repository/commits", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project.repository.commit.id)
@@ -29,7 +29,7 @@ describe API::API, api: true do
context "unauthorized user" do
it "should not return project commits" do
get api("/projects/#{project.id}/repository/commits")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -63,7 +63,7 @@ describe API::API, api: true do
it "should return an invalid parameter error message" do
get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format"
end
end
@@ -73,34 +73,34 @@ describe API::API, api: true do
context "authorized user" do
it "should return a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project.repository.commit.id)
expect(json_response['title']).to eq(project.repository.commit.title)
end
it "should return a 404 error if not found" do
get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return nil for commit without CI" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['status']).to be_nil
end
it "should return status for CI" do
- ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master')
+ pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response.status).to eq(200)
- expect(json_response['status']).to eq(ci_commit.status)
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq(pipeline.status)
end
end
context "unauthorized user" do
it "should not return the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -111,7 +111,7 @@ describe API::API, api: true do
it "should return the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to be >= 1
@@ -120,14 +120,14 @@ describe API::API, api: true do
it "should return a 404 error if invalid commit" do
get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
it "should not return the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -136,7 +136,7 @@ describe API::API, api: true do
context 'authorized user' do
it 'should return merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq('a comment on a commit')
@@ -145,14 +145,14 @@ describe API::API, api: true do
it 'should return a 404 error if merge_request_id not found' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
it 'should not return the diff of the selected commit' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -161,7 +161,7 @@ describe API::API, api: true do
context 'authorized user' do
it 'should return comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to be_nil
expect(json_response['line']).to be_nil
@@ -170,7 +170,7 @@ describe API::API, api: true do
it 'should return the inline comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path)
expect(json_response['line']).to eq(7)
@@ -179,19 +179,19 @@ describe API::API, api: true do
it 'should return 400 if note is missing' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 404 if note is attached to non existent commit' do
post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
it 'should not return the diff of the selected commit' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 0afc3e79339..5262a623761 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -7,25 +7,24 @@ describe API::API, api: true do
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id }
-
describe "when unauthenticated" do
it "returns authentication success" do
get api("/user"), access_token: token.token
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
describe "when token invalid" do
it "returns authentication error" do
get api("/user"), access_token: "123a"
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
describe "authorization by private token" do
it "returns authentication success" do
get api("/user", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 8efa09f75fd..2e5448143d5 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
@@ -25,7 +25,7 @@ describe API::API, api: true do
it "should return a 400 bad request if no params given" do
get api("/projects/#{project.id}/repository/files", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 404 if such file does not exist" do
@@ -35,7 +35,7 @@ describe API::API, api: true do
}
get api("/projects/#{project.id}/repository/files", user), params
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -51,13 +51,13 @@ describe API::API, api: true do
it "should create a new file in project repo" do
post api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
end
it "should return a 400 bad request if no params given" do
post api("/projects/#{project.id}/repository/files", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 400 if editor fails to create file" do
@@ -65,7 +65,7 @@ describe API::API, api: true do
and_return(false)
post api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -81,13 +81,13 @@ describe API::API, api: true do
it "should update existing file in project repo" do
put api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
it "should return a 400 bad request if no params given" do
put api("/projects/#{project.id}/repository/files", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -102,20 +102,20 @@ describe API::API, api: true do
it "should delete existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
it "should return a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -143,7 +143,7 @@ describe API::API, api: true do
it "remains unchanged" do
get api("/projects/#{project.id}/repository/files", user), get_params
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq(file_path)
expect(json_response['content']).to eq(put_params[:content])
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index fa94e03ec32..a9f5aa924b7 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -22,7 +22,7 @@ describe API::API, api: true do
context 'when authenticated' do
it 'should fork if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(user2.id)
@@ -32,7 +32,7 @@ describe API::API, api: true do
it 'should fork if user is admin' do
post api("/projects/fork/#{project.id}", admin)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(admin.id)
@@ -42,20 +42,20 @@ describe API::API, api: true do
it 'should fail on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'should fail if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
end
it 'should fail if project to fork from does not exist' do
post api('/projects/fork/424242', user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
@@ -63,7 +63,7 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
post api("/projects/fork/#{project.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
end
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
index 96d89e69209..52f9e7d4681 100644
--- a/spec/requests/api/group_members_spec.rb
+++ b/spec/requests/api/group_members_spec.rb
@@ -31,21 +31,21 @@ describe API::API, api: true do
it "each user: should return an array of members groups of group3" do
[owner, master, developer, reporter, guest].each do |user|
get api("/groups/#{group_with_members.id}/members", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(5)
- expect(json_response.find { |e| e['id']==owner.id }['access_level']).to eq(GroupMember::OWNER)
- expect(json_response.find { |e| e['id']==reporter.id }['access_level']).to eq(GroupMember::REPORTER)
- expect(json_response.find { |e| e['id']==developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
- expect(json_response.find { |e| e['id']==master.id }['access_level']).to eq(GroupMember::MASTER)
- expect(json_response.find { |e| e['id']==guest.id }['access_level']).to eq(GroupMember::GUEST)
+ expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
+ expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
+ expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
+ expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
+ expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
end
end
it 'users not part of the group should get access error' do
get api("/groups/#{group_with_members.id}/members", stranger)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -54,7 +54,7 @@ describe API::API, api: true do
context "when not a member of the group" do
it "should not add guest as member of group_no_members when adding being done by person outside the group" do
post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -66,7 +66,7 @@ describe API::API, api: true do
post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
end.to change { group_no_members.members.count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq(new_user.name)
expect(json_response['access_level']).to eq(GroupMember::MASTER)
end
@@ -78,27 +78,27 @@ describe API::API, api: true do
post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
end.not_to change { group_with_members.members.count }
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should return error if member already exists" do
post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
end
it "should return a 400 error when user id is not given" do
post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 400 error when access level is not given" do
post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 422 error when access level is not known" do
post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
end
@@ -110,7 +110,7 @@ describe API::API, api: true do
api("/groups/#{group_no_members.id}/members/#{developer.id}",
owner), access_level: GroupMember::MASTER
)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -122,7 +122,7 @@ describe API::API, api: true do
access_level: GroupMember::MASTER
)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
get api("/groups/#{group_with_members.id}/members", owner)
json_reporter = json_response.find do |e|
@@ -139,7 +139,7 @@ describe API::API, api: true do
access_level: GroupMember::MASTER
)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
get api("/groups/#{group_with_members.id}/members", owner)
json_developer = json_response.find do |e|
@@ -153,7 +153,7 @@ describe API::API, api: true do
put(
api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return a 422 error when access level is not known' do
@@ -161,7 +161,7 @@ describe API::API, api: true do
api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
access_level: 1234
)
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
end
@@ -172,7 +172,7 @@ describe API::API, api: true do
random_user = create(:user)
delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -182,17 +182,17 @@ describe API::API, api: true do
delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
end.to change { group_with_members.members.count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return a 404 error when user id is not known" do
delete api("/groups/#{group_with_members.id}/members/1328", owner)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should not allow guest to modify group members" do
delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 37ddab83c30..c2c94040ece 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -12,6 +12,7 @@ describe API::API, api: true do
let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
+ let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -22,14 +23,14 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/groups")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when authenticated as user" do
it "normal user: should return an array of groups of user1" do
get api("/groups", user1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(group1.name)
@@ -39,7 +40,7 @@ describe API::API, api: true do
context "when authenticated as admin" do
it "admin: should return an array of all groups" do
get api("/groups", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -48,53 +49,68 @@ describe API::API, api: true do
describe "GET /groups/:id" do
context "when authenticated as user" do
- it "should return one of user1's groups" do
+ it "returns one of user1's groups" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
get api("/groups/#{group1.id}", user1)
- expect(response.status).to eq(200)
- json_response['name'] == group1.name
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(group1.id)
+ expect(json_response['name']).to eq(group1.name)
+ expect(json_response['path']).to eq(group1.path)
+ expect(json_response['description']).to eq(group1.description)
+ expect(json_response['visibility_level']).to eq(group1.visibility_level)
+ expect(json_response['avatar_url']).to eq(group1.avatar_url)
+ expect(json_response['web_url']).to eq(group1.web_url)
+ expect(json_response['projects']).to be_an Array
+ expect(json_response['projects'].length).to eq(2)
+ expect(json_response['shared_projects']).to be_an Array
+ expect(json_response['shared_projects'].length).to eq(1)
+ expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end
it "should not return a non existing group" do
get api("/groups/1328", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context "when authenticated as admin" do
it "should return any existing group" do
get api("/groups/#{group2.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
it "should not return a non existing group" do
get api("/groups/1328", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context 'when using group path in URL' do
it 'should return any existing group' do
get api("/groups/#{group1.path}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
it 'should not return a non existing group' do
get api('/groups/unknown', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -106,14 +122,14 @@ describe API::API, api: true do
it 'updates the group' do
put api("/groups/#{group1.id}", user1), name: new_group_name
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name)
end
it 'returns 404 for a non existing group' do
put api('/groups/1328', user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -121,7 +137,7 @@ describe API::API, api: true do
it 'updates the group' do
put api("/groups/#{group1.id}", admin), name: new_group_name
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name)
end
end
@@ -130,7 +146,7 @@ describe API::API, api: true do
it 'does not updates the group' do
put api("/groups/#{group1.id}", user2), name: new_group_name
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -138,7 +154,7 @@ describe API::API, api: true do
it 'returns 404 when trying to update the group' do
put api("/groups/#{group2.id}", user1), name: new_group_name
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -147,53 +163,67 @@ describe API::API, api: true do
context "when authenticated as user" do
it "should return the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
- expect(response.status).to eq(200)
- expect(json_response.length).to eq(1)
- expect(json_response.first['name']).to eq(project1.name)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
end
it "should not return a non existing group" do
get api("/groups/1328/projects", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
+ end
+
+ it "should only return projects to which user has access" do
+ project3.team << [user3, :developer]
+
+ get api("/groups/#{group1.id}/projects", user3)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project3.name)
end
end
context "when authenticated as admin" do
it "should return any existing group" do
get api("/groups/#{group2.id}/projects", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
it "should not return a non existing group" do
get api("/groups/1328/projects", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context 'when using group path in URL' do
it 'should return any existing group' do
get api("/groups/#{group1.path}/projects", admin)
- expect(response.status).to eq(200)
- expect(json_response.first['name']).to eq(project1.name)
+
+ expect(response).to have_http_status(200)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
end
it 'should not return a non existing group' do
get api('/groups/unknown/projects', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -202,30 +232,30 @@ describe API::API, api: true do
context "when authenticated as user without group permissions" do
it "should not create group" do
post api("/groups", user1), attributes_for(:group)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
context "when authenticated as user with group permissions" do
it "should create group" do
post api("/groups", user3), attributes_for(:group)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it "should not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(response.message).to eq("Bad Request")
end
it "should return 400 bad request error if name not given" do
post api("/groups", user3), { path: group2.path }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
end
@@ -234,37 +264,37 @@ describe API::API, api: true do
context "when authenticated as user" do
it "should remove group" do
delete api("/groups/#{group1.id}", user1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should not remove a group if not an owner" do
user4 = create(:user)
group1.add_master(user4)
delete api("/groups/#{group1.id}", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should not remove a non existing group" do
delete api("/groups/1328", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context "when authenticated as admin" do
it "should remove any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should not remove a non existing group" do
delete api("/groups/1328", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -280,14 +310,14 @@ describe API::API, api: true do
context "when authenticated as user" do
it "should not transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
it "should transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 22802dd0e05..e567d36afa8 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -11,7 +11,7 @@ describe API::API, api: true do
it do
get api("/internal/check"), secret_token: secret_token
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['api_version']).to eq(API::API.version)
end
end
@@ -23,7 +23,7 @@ describe API::API, api: true do
it do
get api("/internal/broadcast_message"), secret_token: secret_token
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["message"]).to eq(broadcast_message.message)
end
end
@@ -32,7 +32,7 @@ describe API::API, api: true do
it do
get api("/internal/broadcast_message"), secret_token: secret_token
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_empty
end
end
@@ -42,7 +42,7 @@ describe API::API, api: true do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(user.name)
end
@@ -61,7 +61,7 @@ describe API::API, api: true do
push(key, project_wiki)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
end
end
@@ -70,8 +70,9 @@ describe API::API, api: true do
it do
pull(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
end
end
@@ -79,8 +80,9 @@ describe API::API, api: true do
it do
push(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
end
end
end
@@ -94,7 +96,7 @@ describe API::API, api: true do
it do
pull(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -103,7 +105,7 @@ describe API::API, api: true do
it do
push(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -120,7 +122,7 @@ describe API::API, api: true do
it do
pull(key, personal_project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -129,7 +131,7 @@ describe API::API, api: true do
it do
push(key, personal_project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -147,7 +149,7 @@ describe API::API, api: true do
it do
pull(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
end
end
@@ -156,7 +158,7 @@ describe API::API, api: true do
it do
push(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -173,7 +175,7 @@ describe API::API, api: true do
it do
archive(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
end
end
@@ -182,7 +184,7 @@ describe API::API, api: true do
it do
archive(key, project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -192,7 +194,7 @@ describe API::API, api: true do
it do
pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists'))
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -201,30 +203,90 @@ describe API::API, api: true do
it do
pull(OpenStruct.new(id: 0), project)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
+
+ context 'ssh access has been disabled' do
+ before do
+ settings = ::ApplicationSetting.create_from_defaults
+ settings.update_attribute(:enabled_git_access_protocol, 'http')
+ end
+
+ it 'rejects the SSH push' do
+ push(key, project)
+
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq 'Git access over SSH is not allowed'
+ end
+
+ it 'rejects the SSH pull' do
+ pull(key, project)
+
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq 'Git access over SSH is not allowed'
+ end
+ end
+
+ context 'http access has been disabled' do
+ before do
+ settings = ::ApplicationSetting.create_from_defaults
+ settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ end
+
+ it 'rejects the HTTP push' do
+ push(key, project, 'http')
+
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
+ end
+
+ it 'rejects the HTTP pull' do
+ pull(key, project, 'http')
+
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
+ end
+ end
+
+ context 'web actions are always allowed' do
+ it 'allows WEB push' do
+ settings = ::ApplicationSetting.create_from_defaults
+ settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ project.team << [user, :developer]
+ push(key, project, 'web')
+
+ expect(response.status).to eq(200)
+ expect(json_response['status']).to be_truthy
+ end
+ end
end
- def pull(key, project)
+ def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
key_id: key.id,
project: project.path_with_namespace,
action: 'git-upload-pack',
- secret_token: secret_token
+ secret_token: secret_token,
+ protocol: protocol
)
end
- def push(key, project)
+ def push(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
project: project.path_with_namespace,
action: 'git-receive-pack',
- secret_token: secret_token
+ secret_token: secret_token,
+ protocol: protocol
)
end
@@ -235,7 +297,8 @@ describe API::API, api: true do
key_id: key.id,
project: project.path_with_namespace,
action: 'git-upload-archive',
- secret_token: secret_token
+ secret_token: secret_token,
+ protocol: 'ssh'
)
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9dd43f4fab3..6adccb4ebae 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -5,6 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
@@ -41,20 +42,23 @@ describe API::API, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- before { project.team << [user, :reporter] }
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
describe "GET /issues" do
context "when unauthenticated" do
it "should return authentication error" do
get api("/issues")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "should return an array of issues" do
get api("/issues", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
end
@@ -68,7 +72,7 @@ describe API::API, api: true do
it 'should return an array of closed issues' do
get api('/issues?state=closed', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -76,7 +80,7 @@ describe API::API, api: true do
it 'should return an array of opened issues' do
get api('/issues?state=opened', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -84,7 +88,7 @@ describe API::API, api: true do
it 'should return an array of all issues' do
get api('/issues?state=all', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -93,7 +97,7 @@ describe API::API, api: true do
it 'should return an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -101,7 +105,7 @@ describe API::API, api: true do
it 'should return an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -109,14 +113,14 @@ describe API::API, api: true do
it 'should return an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'should return an array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -125,20 +129,170 @@ describe API::API, api: true do
it 'should return an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
end
end
+ describe "GET /groups/:id/issues" do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignee: assignee
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ milestone: group_milestone
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ before do
+ group_project.team << [user, :reporter]
+ end
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ it 'returns group issues without confidential issues for non project members' do
+ get api(base_url, non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(group_issue.title)
+ end
+
+ it 'returns group confidential issues for author' do
+ get api(base_url, author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get api(base_url, assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for admin' do
+ get api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns an array of labeled group issues' do
+ get api("#{base_url}?labels=#{group_label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no group issue matches labels' do
+ get api("#{base_url}?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("#{base_url}?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("#{base_url}?milestone=#{group_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("#{base_url}?milestone=#{group_milestone.title}"\
+ '&state=closed', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_closed_issue.id)
+ end
+ end
+
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
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(response).to have_http_status(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 issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -146,7 +300,7 @@ describe API::API, api: true do
it 'should return project confidential issues for author' do
get api("#{base_url}/issues", author)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -154,7 +308,7 @@ describe API::API, api: true do
it 'should return project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -162,7 +316,7 @@ describe API::API, api: true do
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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -170,7 +324,7 @@ describe API::API, api: true do
it 'should return project confidential issues for admin' do
get api("#{base_url}/issues", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -178,7 +332,7 @@ describe API::API, api: true do
it 'should return an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -186,7 +340,7 @@ describe API::API, api: true do
it 'should return an array of labeled project issues when at least one label matches' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -194,28 +348,28 @@ describe API::API, api: true do
it 'should return an empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'should return an empty array if no issue matches milestone' do
get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'should return an empty array if milestone does not exist' do
get api("#{base_url}/issues?milestone=foo", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'should return an array of issues in given milestone' do
get api("#{base_url}/issues?milestone=#{title}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -225,7 +379,7 @@ describe API::API, api: true do
it 'should return an array of issues matching state in milestone' do
get api("#{base_url}/issues?milestone=#{milestone.title}"\
'&state=closed', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -236,7 +390,7 @@ describe API::API, api: true do
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
@@ -249,13 +403,12 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
- expect(json_response['user_notes_count']).to be(1)
end
it "should return a project issue by id" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
@@ -270,39 +423,44 @@ describe API::API, api: true do
it "should return 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
+ end
+
+ it "should return 404 for project members with guest role" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+ expect(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -313,7 +471,7 @@ describe API::API, api: true do
it "should create a new project issue" do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(['label', 'label2'])
@@ -321,21 +479,25 @@ describe API::API, api: true do
it "should return a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
- it 'should return 400 on invalid label names' do
+ it 'should allow special label names' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue',
- labels: 'label, ?'
- expect(response.status).to eq(400)
- expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
end
it 'should return 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
title: 'g' * 256
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -347,7 +509,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: creation_time
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
@@ -371,7 +533,7 @@ describe API::API, api: true do
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(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
@@ -388,7 +550,7 @@ describe API::API, api: true do
it "should update a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -396,42 +558,53 @@ describe API::API, api: true do
it "should return 404 error if issue id not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
- it 'should return 400 on invalid label names' do
+ it 'should allow special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title',
- labels: 'label, ?'
- expect(response.status).to eq(400)
- expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
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)
+ expect(response).to have_http_status(403)
+ end
+
+ it "should return 403 for project members with guest role" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+ expect(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
end
@@ -444,46 +617,43 @@ describe API::API, api: true do
it 'should not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
it 'should remove all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: ''
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
it 'should update labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'foo,bar'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
end
- it 'should return 400 on invalid label names' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'label, ?'
- expect(response.status).to eq(400)
- expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
- end
-
it 'should allow special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
- labels: 'label:foo, label-bar,label_bar,label/bar'
+ labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
end
it 'should return 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'g' * 256
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -494,7 +664,7 @@ describe API::API, api: true do
it "should update a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label2', state_event: "close"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
@@ -505,7 +675,7 @@ describe API::API, api: true do
update_time = 2.weeks.ago
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time)
@@ -516,12 +686,12 @@ describe API::API, api: true do
describe "DELETE /projects/:id/issues/:issue_id" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
- expect(response.status).to be(403)
+ expect(response).to have_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.id}", author)
- expect(response.status).to be(403)
+ expect(response).to have_http_status(403)
end
context "when the user is project owner" do
@@ -530,7 +700,7 @@ describe API::API, api: true do
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'opened'
end
end
@@ -544,7 +714,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project.id
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
@@ -553,7 +723,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: project.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
@@ -563,7 +733,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project2.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
@@ -572,7 +742,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
to_project_id: target_project2.id
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
@@ -581,7 +751,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/123/move", user),
to_project_id: target_project.id
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -590,7 +760,7 @@ describe API::API, api: true do
post api("/projects/123/issues/#{issue.id}/move", user),
to_project_id: target_project.id
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -599,7 +769,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: 123
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -608,20 +778,26 @@ describe API::API, api: true do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscription", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
end
end
@@ -629,20 +805,26 @@ describe API::API, api: true do
it 'unsubscribes from an issue' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
delete api("/projects/#{project.id}/issues/123/subscription", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index d2b87f88712..1861882d59e 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -14,14 +14,14 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api("/keys/#{key.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'should return 404 for non-existing key' do
get api('/keys/999999', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -29,7 +29,7 @@ describe API::API, api: true do
user.keys << key
user.save
get api("/keys/#{key.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(key.title)
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['user']['username']).to eq(user.username)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 6943ff9d26c..63636b4a1b6 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -11,11 +11,10 @@ describe API::API, api: true do
project.team << [user, :master]
end
-
describe 'GET /projects/:id/labels' do
it 'should return project labels' do
get api("/projects/#{project.id}/labels", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(label1.name)
@@ -28,7 +27,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAABB',
description: 'test'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq('Foo')
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
@@ -36,29 +35,29 @@ describe API::API, api: true do
it 'should return created label when only required params' do
post api("/projects/#{project.id}/labels", user),
- name: 'Foo',
+ name: 'Foo & Bar',
color: '#FFAABB'
expect(response.status).to eq(201)
- expect(json_response['name']).to eq('Foo')
+ expect(json_response['name']).to eq('Foo & Bar')
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to be_nil
end
it 'should return a 400 bad request if name not given' do
post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return a 400 bad request if color not given' do
post api("/projects/#{project.id}/labels", user), name: 'Foobar'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 for invalid color' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAA'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -66,15 +65,15 @@ describe API::API, api: true do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for invalid name' do
post api("/projects/#{project.id}/labels", user),
- name: '?',
+ name: ',',
color: '#FFAABB'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid'])
end
@@ -82,7 +81,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFAABB'
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Label already exists')
end
end
@@ -90,18 +89,18 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/labels' do
it 'should return 200 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return 404 for non existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label2'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
end
it 'should return 400 for wrong parameters' do
delete api("/projects/#{project.id}/labels", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -112,7 +111,7 @@ describe API::API, api: true do
new_name: 'New Label',
color: '#FFFFFF',
description: 'test'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq('New Label')
expect(json_response['color']).to eq('#FFFFFF')
expect(json_response['description']).to eq('test')
@@ -122,7 +121,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq('New Label')
expect(json_response['color']).to eq(label1.color)
end
@@ -131,7 +130,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFFFFF'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(label1.name)
expect(json_response['color']).to eq('#FFFFFF')
end
@@ -140,7 +139,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
description: 'test'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(label1.name)
expect(json_response['description']).to eq('test')
end
@@ -149,18 +148,18 @@ describe API::API, api: true do
put api("/projects/#{project.id}/labels", user),
name: 'label2',
new_name: 'label3'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should return 400 if no label name given' do
put api("/projects/#{project.id}/labels", user), new_name: 'label2'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "name" not given')
end
it 'should return 400 if no new parameters given' do
put api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Required parameters '\
'"new_name" or "color" missing')
end
@@ -168,9 +167,9 @@ describe API::API, api: true do
it 'should return 400 for invalid name' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
- new_name: '?',
+ new_name: ',',
color: '#FFFFFF'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid'])
end
@@ -178,7 +177,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -186,8 +185,90 @@ describe API::API, api: true do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
end
+
+ describe "POST /projects/:id/labels/:label_id/subscription" do
+ context "when label_id is a label title" do
+ it "should subscribe to the label" do
+ post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "should subscribe to the label" do
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when user is already subscribed to label" do
+ before { label1.subscribe(user) }
+
+ it "should return 304" do
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "should a return 404 error" do
+ post api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/labels/:label_id/subscription" do
+ before { label1.subscribe(user) }
+
+ context "when label_id is a label title" do
+ it "should unsubscribe from the label" do
+ delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "should unsubscribe from the label" do
+ delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when user is already unsubscribed from label" do
+ before { label1.unsubscribe(user) }
+
+ it "should return 304" do
+ delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "should a return 404 error" do
+ delete api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/license_templates_spec.rb
index c17dcb222a9..9a1894d63a2 100644
--- a/spec/requests/api/licenses_spec.rb
+++ b/spec/requests/api/license_templates_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Licenses, api: true do
+describe API::API, api: true do
include ApiHelpers
describe 'Entity' do
@@ -23,7 +23,7 @@ describe API::Licenses, api: true do
it 'returns a list of available license templates' do
get api('/licenses')
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(15)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
@@ -34,7 +34,7 @@ describe API::Licenses, api: true do
it 'returns a list of available popular license templates' do
get api('/licenses?popular=1')
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
@@ -57,7 +57,7 @@ describe API::Licenses, api: true do
end
it 'replaces placeholder values' do
- expect(json_response['content']).to include('Copyright (c) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
end
end
@@ -70,7 +70,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -83,7 +83,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -96,7 +96,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -108,7 +108,7 @@ describe API::Licenses, api: true do
end
it 'replaces placeholder values' do
- expect(json_response['content']).to include('Copyright 2016 Anton')
+ expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
end
end
@@ -116,7 +116,7 @@ describe API::Licenses, api: true do
let(:license_type) { 'muth-over9000' }
it 'returns a 404' do
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -128,7 +128,7 @@ describe API::Licenses, api: true do
it 'replaces the copyright owner placeholder with the name of the current user' do
get api('/licenses/mit', user)
- expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}")
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4b0111df149..4a1b5600bdf 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -22,14 +22,14 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/projects/#{project.id}/merge_requests")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "should return an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -37,7 +37,7 @@ describe API::API, api: true do
it "should return an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -45,7 +45,7 @@ describe API::API, api: true do
it "should return an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -53,7 +53,7 @@ describe API::API, api: true do
it "should return an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_closed.title)
@@ -61,7 +61,7 @@ describe API::API, api: true do
it "should return an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_merged.title)
@@ -75,7 +75,7 @@ describe API::API, api: true do
it "should return an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -84,7 +84,7 @@ describe API::API, api: true do
it "should return an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -93,7 +93,7 @@ describe API::API, api: true do
it "should return an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
@@ -102,7 +102,7 @@ describe API::API, api: true do
it "should return an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
@@ -116,7 +116,7 @@ describe API::API, api: true do
it 'exposes known attributes' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['project_id']).to eq(merge_request.project.id)
@@ -138,12 +138,11 @@ describe API::API, api: true do
expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
- expect(json_response['user_notes_count']).to be(2)
end
it "should return merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
@@ -160,7 +159,7 @@ describe API::API, api: true do
it "should return a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
context 'Work in Progress' do
@@ -168,7 +167,7 @@ describe API::API, api: true do
it "should return merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
@@ -187,7 +186,7 @@ describe API::API, api: true do
it 'returns a 404 when merge_request_id not found' do
get api("/projects/#{project.id}/merge_requests/999/commits", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -200,7 +199,7 @@ describe API::API, api: true do
it 'returns a 404 when merge_request_id not found' do
get api("/projects/#{project.id}/merge_requests/999/changes", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -214,7 +213,7 @@ describe API::API, api: true do
author: user,
labels: 'label, label2',
milestone_id: milestone.id
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(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)
@@ -223,38 +222,40 @@ describe API::API, api: true do
it "should return 422 when source_branch equals target_branch" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
it "should return 400 when source_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 400 when target_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 400 when title is missing" do
post api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
- it 'should return 400 on invalid label names' do
+ it 'should allow special label names' do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'markdown',
target_branch: 'master',
author: user,
- labels: 'label, ?'
- expect(response.status).to eq(400)
- expect(json_response['message']['labels']['?']['title']).to eq(
- ['is invalid']
- )
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
end
context 'with existing MR' do
@@ -275,7 +276,7 @@ describe API::API, api: true do
target_branch: 'master',
author: user
end.to change { MergeRequest.count }.by(0)
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
end
end
end
@@ -293,7 +294,7 @@ describe API::API, api: true do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['description']).to eq('Test description for Test merge_request')
end
@@ -304,26 +305,26 @@ describe API::API, api: true do
expect(fork_project.forked_from_project).to eq(project)
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
end
it "should return 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 400 when target_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 400 when title is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
context 'when target_branch is specified' do
@@ -334,7 +335,7 @@ describe API::API, api: true do
source_branch: 'markdown',
author: user,
target_project_id: fork_project.id
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
it 'should return 422 if targeting a different fork' do
@@ -344,14 +345,14 @@ describe API::API, api: true do
source_branch: 'markdown',
author: user2,
target_project_id: unrelated_project.id
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
it "should return 201 when target_branch is specified and for the same project" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
end
end
@@ -366,7 +367,7 @@ describe API::API, api: true do
it "denies the deletion of the merge request" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
- expect(response.status).to be(403)
+ expect(response).to have_http_status(403)
end
end
@@ -374,7 +375,7 @@ describe API::API, api: true do
it "destroys the merge request owners can destroy" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
@@ -382,18 +383,18 @@ describe API::API, api: true 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_requests/#{merge_request.id}", user), state_event: "close"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
end
end
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
- let(:ci_commit) { create(:ci_commit_without_jobs) }
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
it "should return merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return 406 if branch can't be merged" do
@@ -402,21 +403,30 @@ describe API::API, api: true do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response.status).to eq(406)
+ expect(response).to have_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
end
it "should return 405 if merge_request is not open" do
merge_request.close
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(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_requests/#{merge_request.id}/merge", user)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -424,17 +434,30 @@ describe API::API, api: true do
user2 = create(:user)
project.team << [user2, :reporter]
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
+ it "returns 409 if the SHA parameter doesn't match" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
+ end
+
+ it "succeeds if the SHA parameter matches" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+
+ expect(response).to have_http_status(200)
+ end
+
it "enables merge when build succeeds if the ci is active" do
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:active?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(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(response).to have_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_build_succeeds']).to eq(true)
end
@@ -443,41 +466,45 @@ describe API::API, api: true do
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(response).to have_http_status(200)
expect(json_response['title']).to eq('New title')
end
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(response).to have_http_status(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(response).to have_http_status(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_requests/#{merge_request.id}", user),
source_branch: "master", target_branch: "master"
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
- it 'should return 400 on invalid label names' do
+ it 'should allow special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
user),
title: 'new issue',
- labels: 'label, ?'
- expect(response.status).to eq(400)
- expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
end
end
@@ -486,7 +513,7 @@ describe API::API, api: true do
original_count = merge_request.notes.size
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['author']['name']).to eq(user.name)
expect(json_response['author']['username']).to eq(user.username)
@@ -495,20 +522,20 @@ describe API::API, api: true do
it "should return 400 if note is missing" do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return 404 if note is attached to non existent merge request" do
post api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
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_requests/#{merge_request.id}/comments", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq("a comment on a MR")
@@ -518,7 +545,7 @@ describe API::API, api: true do
it "should return a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999/comments", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -530,7 +557,7 @@ describe API::API, api: true do
end
get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -538,30 +565,45 @@ describe API::API, api: true do
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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -569,20 +611,20 @@ describe API::API, api: true do
it 'unsubscribes from a merge request' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 241995041bb..0f4e38b2475 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -12,20 +12,20 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones' do
it 'should return project milestones' do
get api("/projects/#{project.id}/milestones", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
end
it 'should return a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it 'returns an array of active milestones' do
get api("/projects/#{project.id}/milestones?state=active", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(milestone.id)
@@ -34,7 +34,7 @@ describe API::API, api: true do
it 'returns an array of closed milestones' do
get api("/projects/#{project.id}/milestones?state=closed", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_milestone.id)
@@ -44,7 +44,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones/:milestone_id' do
it 'should return a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
@@ -60,19 +60,19 @@ describe API::API, api: true do
it 'should return 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it 'should return a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/milestones' do
it 'should create a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
@@ -80,14 +80,14 @@ describe API::API, api: true do
it 'should create a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
end
it 'should return a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -95,14 +95,14 @@ describe API::API, api: true do
it 'should update a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'should return a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -110,7 +110,7 @@ describe API::API, api: true do
it 'should update a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
end
@@ -131,14 +131,14 @@ describe API::API, api: true do
end
it 'should return project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
it 'should return a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
describe 'confidential issues' do
@@ -146,6 +146,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
before do
public_project.team << [user, :developer]
milestone.issues << issue << confidential_issue
@@ -154,16 +155,28 @@ describe API::API, api: true do
it 'returns confidential issues to team members' do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
end
+ it 'does not return confidential issues to team members with guest role' do
+ member = create(:user)
+ project.team << [member, :guest]
+
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+
it 'does not return confidential issues to regular users' do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 21787fdd895..237b4b17eb5 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -11,14 +11,14 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/namespaces")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when authenticated as admin" do
it "admin: should return an array of all namespaces" do
get api("/namespaces", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(Namespace.count)
@@ -26,7 +26,7 @@ describe API::API, api: true do
it "admin: should return an array of matched namespaces" do
get api("/namespaces?search=#{group1.name}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -36,7 +36,7 @@ describe API::API, api: true do
context "when authenticated as a regular user" do
it "user: should return an array of namespaces" do
get api("/namespaces", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -44,7 +44,7 @@ describe API::API, api: true do
it "admin: should return an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 49091fc0f49..65c53211dd3 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace) }
+ let!(:project) { create(:project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
let!(:snippet) { create(:project_snippet, project: project, author: user) }
@@ -39,28 +39,42 @@ describe API::API, api: true do
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)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(issue_note.note)
end
it "should return a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/notes", user)
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
- context "that references a private issue" do
+ context "and current user cannot view the notes" 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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
+ context "and issue is confidential" do
+ before { ext_issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+ 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(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
end
@@ -71,28 +85,44 @@ describe API::API, api: true do
context "when noteable is a Snippet" do
it "should return an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(snippet_note.note)
end
it "should return a 404 error when snippet id not found" do
get api("/projects/#{project.id}/snippets/42/notes", user)
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
+
+ expect(response).to have_http_status(404)
end
end
context "when noteable is a Merge Request" do
it "should return an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(merge_request_note.note)
end
it "should return a 404 error if merge request id not found" do
get api("/projects/#{project.id}/merge_requests/4444/notes", user)
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -101,25 +131,39 @@ describe API::API, api: true do
context "when noteable is an Issue" do
it "should return an issue note by id" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
it "should return a 404 error if issue note not found" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
- context "that references a private issue" do
+ context "and current user cannot view the note" 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)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when issue is confidential" do
+ before { issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+
+ expect(response).to have_http_status(404)
+ end
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(response).to have_http_status(200)
expect(json_response['body']).to eq(cross_reference_note.note)
end
end
@@ -129,13 +173,15 @@ describe API::API, api: true do
context "when noteable is a Snippet" do
it "should return a snippet note by id" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response['body']).to eq(snippet_note.note)
end
it "should return a 404 error if snippet note not found" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -144,19 +190,22 @@ describe API::API, api: true do
context "when noteable is an Issue" do
it "should create a new issue note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
- expect(response.status).to eq(201)
+
+ expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
it "should return a 400 bad request error if body not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
- expect(response.status).to eq(400)
+
+ expect(response).to have_http_status(400)
end
it "should return a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
- expect(response.status).to eq(401)
+
+ expect(response).to have_http_status(401)
end
context 'when an admin or owner makes the request' do
@@ -164,31 +213,34 @@ describe API::API, api: true do
creation_time = 2.weeks.ago
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'hi!', created_at: creation_time
- expect(response.status).to eq(201)
+
+ expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time)
end
end
-
end
context "when noteable is a Snippet" do
it "should create a new snippet note" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
- expect(response.status).to eq(201)
+
+ expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
it "should return a 400 bad request error if body not given" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response.status).to eq(400)
+
+ expect(response).to have_http_status(400)
end
it "should return a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
- expect(response.status).to eq(401)
+
+ expect(response).to have_http_status(401)
end
end
@@ -204,8 +256,8 @@ describe API::API, api: true do
body: 'Hi!'
end
- it 'responds with 500' do
- expect(response.status).to eq 500
+ it 'responds with resource not found error' do
+ expect(response.status).to eq 404
end
it 'does not create new note' do
@@ -227,20 +279,23 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
body: 'Hello!'
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
it 'should return a 400 bad request error if body not given' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response.status).to eq(400)
+
+ expect(response).to have_http_status(400)
end
end
@@ -248,14 +303,16 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
end
@@ -263,14 +320,16 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/12345", user), body: "Hello!"
- expect(response.status).to eq(404)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -281,17 +340,17 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
# Check if note is really deleted
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -300,18 +359,18 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
# Check if note is really deleted
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -320,20 +379,19 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
# Check if note is really deleted
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/12345", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
-
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index ffb93bbb120..fd1fffa6223 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -22,7 +22,7 @@ describe API::API, 'ProjectHooks', api: true do
context "authorized user" do
it "should return project hooks" do
get api("/projects/#{project.id}/hooks", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
@@ -40,7 +40,7 @@ describe API::API, 'ProjectHooks', api: true do
context "unauthorized user" do
it "should not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -49,7 +49,7 @@ describe API::API, 'ProjectHooks', api: true do
context "authorized user" do
it "should return a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(hook.push_events)
@@ -61,20 +61,20 @@ describe API::API, 'ProjectHooks', api: true do
it "should return a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
it "should not access an existing hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
it "should return a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -83,7 +83,7 @@ describe API::API, 'ProjectHooks', api: true do
expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true)
@@ -96,12 +96,12 @@ describe API::API, 'ProjectHooks', api: true do
it "should return a 400 error if url not given" do
post api("/projects/#{project.id}/hooks", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 422 error if url not valid" do
post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
@@ -109,7 +109,7 @@ describe API::API, 'ProjectHooks', api: true do
it "should update an existing project hook" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(false)
@@ -121,17 +121,17 @@ describe API::API, 'ProjectHooks', api: true do
it "should return 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return 400 error if url is not given" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 422 error if url is not valid" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
@@ -140,22 +140,22 @@ describe API::API, 'ProjectHooks', api: true do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
end.to change {project.hooks.count}.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return success when deleting hook" do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return a 404 error when deleting non existent hook" do
delete api("/projects/#{project.id}/hooks/42", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return a 405 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
end
it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do
@@ -164,7 +164,7 @@ describe API::API, 'ProjectHooks', api: true do
other_project.team << [test_user, :master]
delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(WebHook.exists?(hook.id)).to be_truthy
end
end
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index c112ca5e3ca..9a7c1da4401 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -15,7 +15,7 @@ describe API::API, api: true do
it "should return project team members" do
get api("/projects/#{project.id}/members", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(2)
expect(json_response.map { |u| u['username'] }).to include user.username
@@ -23,7 +23,7 @@ describe API::API, api: true do
it "finds team members with query string" do
get api("/projects/#{project.id}/members", user), query: user.username
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(user.username)
@@ -31,7 +31,7 @@ describe API::API, api: true do
it "should return a 404 error if id not found" do
get api("/projects/9999/members", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -40,14 +40,14 @@ describe API::API, api: true do
it "should return project team member" do
get api("/projects/#{project.id}/members/#{user.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(json_response['access_level']).to eq(ProjectMember::MASTER)
end
it "should return a 404 error if user id not found" do
get api("/projects/#{project.id}/members/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -57,7 +57,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
end.to change { ProjectMember.count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['username']).to eq(user2.username)
expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
end
@@ -70,24 +70,24 @@ describe API::API, api: true do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
end.not_to change { ProjectMember.count }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['username']).to eq(user2.username)
expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
end
it "should return a 400 error when user id is not given" do
post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 400 error when access level is not given" do
post api("/projects/#{project.id}/members", user), user_id: user2.id
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 422 error when access level is not known" do
post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
@@ -96,24 +96,24 @@ describe API::API, api: true do
it "should update project team member" do
put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user3.username)
expect(json_response['access_level']).to eq(ProjectMember::MASTER)
end
it "should return a 404 error if user_id is not found" do
put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return a 400 error when access level is not given" do
put api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should return a 422 error when access level is not known" do
put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(422)
end
end
@@ -133,21 +133,21 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
expect do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.to_not change { ProjectMember.count }
- expect(response.status).to eq(200)
+ end.not_to change { ProjectMember.count }
+ expect(response).to have_http_status(200)
end
it "should return 200 if team member already removed" do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
delete api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return 200 OK when the user was not member" do
expect do
delete api("/projects/#{project.id}/members/1000000", user)
end.to change { ProjectMember.count }.by(0)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['id']).to eq(1000000)
expect(json_response['message']).to eq('Access revoked')
end
@@ -158,7 +158,7 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/members/#{user3.id}", user3)
end.to change { ProjectMember.count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project_member2.id)
end
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 9706d060cfa..4ebde201941 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -27,7 +27,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/snippets/", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response.size).to eq(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
end
@@ -38,7 +38,7 @@ describe API::API, api: true do
create(:project_snippet, :private, project: project)
get api("/projects/#{project.id}/snippets/", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response.size).to eq(0)
end
end
@@ -56,7 +56,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/snippets/", admin), params
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
snippet = ProjectSnippet.find(json_response['id'])
expect(snippet.content).to eq(params[:code])
expect(snippet.title).to eq(params[:title])
@@ -73,7 +73,7 @@ describe API::API, api: true do
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
snippet.reload
expect(snippet.content).to eq(new_content)
end
@@ -86,7 +86,7 @@ describe API::API, api: true do
delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
@@ -97,7 +97,7 @@ describe API::API, api: true do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 66193eac051..8a52725a893 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -10,20 +10,20 @@ describe API::API, api: true do
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
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, :public, author: user, project: project, title: 'example') }
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,
+ :private,
name: 'second_project',
path: 'second_project',
creator_id: user.id,
namespace: user.namespace,
merge_requests_enabled: false,
issues_enabled: false, wiki_enabled: false,
- snippets_enabled: false, visibility_level: 0)
+ snippets_enabled: false)
end
let(:project_member3) do
create(:project_member,
@@ -45,14 +45,14 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api('/projects')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'should return an array of projects' do
get api('/projects', user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project.name)
expect(json_response.first['owner']['username']).to eq(user.username)
@@ -84,7 +84,7 @@ describe API::API, api: true do
context 'and using search' do
it 'should return searched project' do
get api('/projects', user), { search: project.name }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -93,21 +93,21 @@ describe API::API, api: true do
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(response).to have_http_status(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(response).to have_http_status(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(response).to have_http_status(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
@@ -121,7 +121,7 @@ describe API::API, api: true do
it 'should return the correct order when sorted by id' do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
@@ -135,21 +135,21 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api('/projects/all')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated as regular user' do
it 'should return authentication error' do
get api('/projects/all', user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
context 'when authenticated as admin' do
it 'should return an array of all projects' do
get api('/projects/all', admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to satisfy do |response|
@@ -164,21 +164,18 @@ describe API::API, api: true do
end
describe 'GET /projects/starred' do
+ let(:public_project) { create(:project, :public) }
+
before do
- admin.starred_projects << project
- admin.save!
+ project_member2
+ user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
- it 'should return the starred projects' do
- get api('/projects/all', admin)
- expect(response.status).to eq(200)
+ it 'should return the starred projects viewable by the user' do
+ get api('/projects/starred', user3)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
-
- expect(json_response).to satisfy do |response|
- response.one? do |entry|
- entry['name'] == project.name
- end
- end
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
end
end
@@ -188,25 +185,25 @@ describe API::API, api: true do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post api('/projects', user2), name: 'foo' }.
to change {Project.count}.by(0)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
it 'should create new project without path and return 201' do
expect { post api('/projects', user), name: 'foo' }.
to change { Project.count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it 'should create last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post api('/projects', user2), name: 'foo'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it 'should not create new project without name and return 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should assign attributes to project" do
@@ -220,7 +217,7 @@ describe API::API, api: true do
post api('/projects', user), project
- project.each_pair do |k,v|
+ project.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -276,7 +273,7 @@ describe API::API, api: true do
it 'should not allow a non-admin to use a restricted visibility level' do
post api('/projects', user), @project
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
match('restricted by your GitLab administrator')
)
@@ -298,14 +295,14 @@ describe API::API, api: true do
it 'should create new project without path and return 201' do
expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it 'should respond with 400 on failure and not project' do
expect { post api("/projects/user/#{user.id}", admin) }.
not_to change { Project.count }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq([
'can\'t be blank',
'is too short (minimum is 0 characters)',
@@ -328,7 +325,7 @@ describe API::API, api: true do
post api("/projects/user/#{user.id}", admin), project
- project.each_pair do |k,v|
+ project.each_pair do |k, v|
next if k == :path
expect(json_response[k.to_s]).to eq(v)
end
@@ -383,7 +380,7 @@ describe API::API, api: true do
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(response).to have_http_status(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")
@@ -395,29 +392,65 @@ describe API::API, api: true do
before { project }
before { project_member }
- it 'should return a project by id' do
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
get api("/projects/#{project.id}", user)
- expect(response.status).to eq(200)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['public']).to be_falsey
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility_level']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
- expect(json_response['owner']['username']).to eq(user.username)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['builds_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_builds']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
end
it 'should return a project by path name' do
get api("/projects/#{project.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'should return a 404 error if not found' do
get api('/projects/42', user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'should return a 404 error if user is not a member' do
other_user = create(:user)
get api("/projects/#{project.id}", other_user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should handle users with dots' do
@@ -425,17 +458,18 @@ describe API::API, api: true do
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(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
describe 'permissions' do
context 'all projects' do
- it 'Contains permission information' do
- project.team << [user, :master]
+ before { project.team << [user, :master] }
+
+ it 'contains permission information' do
get api("/projects", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response.first['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
@@ -443,11 +477,11 @@ describe API::API, api: true do
end
context 'personal project' do
- it 'Sets project access and returns 200' do
+ it 'sets project access and returns 200' do
project.team << [user, :master]
get api("/projects/#{project.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
expect(json_response['permissions']['group_access']).to be_nil
@@ -455,12 +489,14 @@ describe API::API, api: true do
end
context 'group project' do
+ let(:project2) { create(:project, group: create(:group)) }
+
+ before { project2.group.add_owner(user) }
+
it 'should set the owner and return 200' do
- project2 = create(:project, group: create(:group))
- project2.group.add_owner(user)
get api("/projects/#{project2.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['permissions']['project_access']).to be_nil
expect(json_response['permissions']['group_access']['access_level']).
to eq(Gitlab::Access::OWNER)
@@ -479,7 +515,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/events", user)
end
- it { expect(response.status).to eq(200) }
+ it { expect(response).to have_http_status(200) }
context 'joined event' do
let(:json_event) { json_response[1] }
@@ -500,14 +536,14 @@ describe API::API, api: true do
it 'should return a 404 error if not found' do
get api('/projects/42/events', user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'should return a 404 error if user is not a member' do
other_user = create(:user)
get api("/projects/#{project.id}/events", other_user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -516,7 +552,7 @@ describe API::API, api: true do
it 'should return an array of project snippets' do
get api("/projects/#{project.id}/snippets", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(snippet.title)
end
@@ -525,13 +561,13 @@ describe API::API, api: true do
describe 'GET /projects/:id/snippets/:snippet_id' do
it 'should return a project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
it 'should return a 404 error if snippet id not found' do
get api("/projects/#{project.id}/snippets/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -540,7 +576,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/snippets", user),
title: 'api test', file_name: 'sample.rb', code: 'test',
visibility_level: '0'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['title']).to eq('api test')
end
@@ -554,7 +590,7 @@ describe API::API, api: true do
it 'should update an existing project snippet' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq('example')
expect(snippet.reload.content).to eq('updated code')
end
@@ -562,7 +598,7 @@ describe API::API, api: true do
it 'should update an existing project snippet with new title' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other api test'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq('other api test')
end
end
@@ -574,24 +610,24 @@ describe API::API, api: true do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
end.to change { Snippet.count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return 404 when deleting unknown snippet id' do
delete api("/projects/#{project.id}/snippets/1234", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
it 'should get a raw project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return a 404 error if raw project snippet not found' do
get api("/projects/#{project.id}/snippets/5555/raw", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -604,7 +640,7 @@ describe API::API, api: true do
it 'should return array of ssh keys' do
get api("/projects/#{project.id}/keys", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
@@ -613,20 +649,20 @@ describe API::API, api: true do
describe 'GET /projects/:id/keys/:key_id' do
it 'should return a single key' do
get api("/projects/#{project.id}/keys/#{deploy_key.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'should return 404 Not Found with invalid ID' do
get api("/projects/#{project.id}/keys/404", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/keys' do
it 'should not create an invalid ssh key' do
post api("/projects/#{project.id}/keys", user), { title: 'invalid key' }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['key']).to eq([
'can\'t be blank',
'is too short (minimum is 0 characters)',
@@ -636,7 +672,7 @@ describe API::API, api: true do
it 'should not create a key without title' do
post api("/projects/#{project.id}/keys", user), key: 'some key'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'can\'t be blank',
'is too short (minimum is 0 characters)'
@@ -662,7 +698,7 @@ describe API::API, api: true do
it 'should return 404 Not Found with invalid ID' do
delete api("/projects/#{project.id}/keys/404", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -676,13 +712,13 @@ describe API::API, api: true do
it "shouldn't available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should allow project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked_project_link).not_to be_nil
@@ -691,7 +727,7 @@ describe API::API, api: true do
it 'should fail if forked_from project which does not exist' do
post api("/projects/#{project_fork_target.id}/fork/9999", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should fail with 409 if already forked' do
@@ -699,7 +735,7 @@ describe API::API, api: true do
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
post api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked?).to be_truthy
@@ -707,10 +743,9 @@ describe API::API, api: true do
end
describe 'DELETE /projects/:id/fork' do
-
it "shouldn't be visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
context 'when users belong to project group' do
@@ -723,7 +758,7 @@ describe API::API, api: true do
it 'should be forbidden to non-owner users' do
delete api("/projects/#{project_fork_target.id}/fork", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should make forked project unforked' do
@@ -732,7 +767,7 @@ describe API::API, api: true do
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
@@ -741,7 +776,7 @@ describe API::API, api: true do
it 'should be idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end
@@ -799,14 +834,14 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api("/projects/search/#{query}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'should return an array of projects' do
- get api("/projects/search/#{query}",user)
- expect(response.status).to eq(200)
+ get api("/projects/search/#{query}", user)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(6)
json_response.each {|project| expect(project['name']).to match(/.*query.*/)}
@@ -816,7 +851,7 @@ describe API::API, api: true do
context 'when authenticated as a different user' do
it 'should return matching public projects' do
get api("/projects/search/#{query}", user2)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)}
@@ -838,7 +873,7 @@ describe API::API, api: true do
it 'should return authentication error' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}"), project_param
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -846,7 +881,7 @@ describe API::API, api: true do
it 'should update name' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}", user), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -855,7 +890,7 @@ describe API::API, api: true do
it 'should update visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -866,7 +901,7 @@ describe API::API, api: true do
project_param = { public: false }
put api("/projects/#{project3.id}", user), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -876,14 +911,14 @@ describe API::API, api: true do
it 'should not update name to existing name' do
project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
it 'should update path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -894,7 +929,7 @@ describe API::API, api: true do
it 'should update path' do
project_param = { path: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -908,7 +943,7 @@ describe API::API, api: true do
description: 'new description' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -917,20 +952,20 @@ describe API::API, api: true do
it 'should not update path to existing path' do
project_param = { path: project.path }
put api("/projects/#{project3.id}", user4), project_param
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
it 'should not update name' do
project_param = { name: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should not update visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user4), project_param
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -943,7 +978,7 @@ describe API::API, api: true do
merge_requests_enabled: true,
description: 'new description' }
put api("/projects/#{project.id}", user3), project_param
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -953,7 +988,7 @@ describe API::API, api: true do
it 'archives the project' do
post api("/projects/#{project.id}/archive", user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -966,7 +1001,7 @@ describe API::API, api: true do
it 'remains archived' do
post api("/projects/#{project.id}/archive", user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -979,7 +1014,7 @@ describe API::API, api: true do
it 'rejects the action' do
post api("/projects/#{project.id}/archive", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -989,7 +1024,7 @@ describe API::API, api: true do
it 'remains unarchived' do
post api("/projects/#{project.id}/unarchive", user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1002,7 +1037,7 @@ describe API::API, api: true do
it 'unarchives the project' do
post api("/projects/#{project.id}/unarchive", user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1015,7 +1050,7 @@ describe API::API, api: true do
it 'rejects the action' do
post api("/projects/#{project.id}/unarchive", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -1025,7 +1060,7 @@ describe API::API, api: true do
it 'stars the project' do
expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['star_count']).to eq(1)
end
end
@@ -1039,7 +1074,7 @@ describe API::API, api: true do
it 'does not modify the star count' do
expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
end
end
@@ -1054,7 +1089,7 @@ describe API::API, api: true do
it 'unstars the project' do
expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['star_count']).to eq(0)
end
end
@@ -1063,7 +1098,7 @@ describe API::API, api: true do
it 'does not modify the star count' do
expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
- expect(response.status).to eq(304)
+ expect(response).to have_http_status(304)
end
end
end
@@ -1072,36 +1107,36 @@ describe API::API, api: true do
context 'when authenticated as user' do
it 'should remove project' do
delete api("/projects/#{project.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
delete api("/projects/#{project.id}", user3)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should not remove a non existing project' do
delete api('/projects/1328', user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should not remove a project not attached to user' do
delete api("/projects/#{project.id}", user2)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
context 'when authenticated as admin' do
it 'should remove any existing project' do
delete api("/projects/#{project.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should not remove a non existing project' do
delete api('/projects/1328', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 7cf4a01d76b..5890e9c9d3d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
it "should return project commits" do
get api("/projects/#{project.id}/repository/tree", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq('encoding')
@@ -28,7 +28,7 @@ describe API::API, api: true do
it 'should return a 404 for unknown ref' do
get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response).to be_an Object
json_response['message'] == '404 Tree Not Found'
@@ -38,7 +38,7 @@ describe API::API, api: true do
context "unauthorized user" do
it "should not return project commits" do
get api("/projects/#{project.id}/repository/tree")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -46,41 +46,41 @@ describe API::API, api: true do
describe "GET /projects/:id/repository/blobs/:sha" do
it "should get the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return 404 for invalid branch_name" do
get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return 404 for invalid file" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return a 400 error if filepath is missing" do
get api("/projects/#{project.id}/repository/blobs/master", user)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
describe "GET /projects/:id/repository/commits/:sha/blob" do
it "should get the raw file contents" do
get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
describe "GET /projects/:id/repository/raw_blobs/:sha" do
it "should get the raw file contents" do
get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return a 404 for unknown blob' do
get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response).to be_an Object
json_response['message'] == '404 Blob Not Found'
@@ -91,7 +91,7 @@ describe API::API, api: true do
it "should get the archive" do
get api("/projects/#{project.id}/repository/archive", user)
repo_name = project.repository.name.gsub("\.git", "")
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
@@ -100,7 +100,7 @@ describe API::API, api: true do
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(response).to have_http_status(200)
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
@@ -109,7 +109,7 @@ describe API::API, api: true do
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(response).to have_http_status(200)
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
@@ -117,28 +117,28 @@ describe API::API, api: true do
it "should return 404 for invalid sha" do
get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/repository/compare' do
it "should compare branches" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "should compare tags" do
get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "should compare commits" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_falsey
@@ -146,14 +146,14 @@ describe API::API, api: true do
it "should compare commits in reverse order" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "should compare same refs" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_truthy
@@ -163,7 +163,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/repository/contributors' do
it 'should return valid data' do
get api("/projects/#{project.id}/repository/contributors", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
contributor = json_response.first
expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com')
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 3af61d4b335..00a3c917b6a 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -39,7 +39,7 @@ describe API::Runners, api: true do
get api('/runners', user)
shared = json_response.any?{ |r| r['is_shared'] }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
@@ -48,14 +48,14 @@ describe API::Runners, api: true do
get api('/runners?scope=active', user)
shared = json_response.any?{ |r| r['is_shared'] }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(400)
end
end
@@ -63,7 +63,7 @@ describe API::Runners, api: true do
it 'should not return runners' do
get api('/runners')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -75,7 +75,7 @@ describe API::Runners, api: true do
get api('/runners/all', admin)
shared = json_response.any?{ |r| r['is_shared'] }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -85,7 +85,7 @@ describe API::Runners, api: true do
it 'should not return runners list' do
get api('/runners/all', user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -93,14 +93,14 @@ describe API::Runners, api: true do
get api('/runners/all?scope=specific', admin)
shared = json_response.any?{ |r| r['is_shared'] }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(400)
end
end
@@ -108,7 +108,7 @@ describe API::Runners, api: true do
it 'should not return runners' do
get api('/runners')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -119,7 +119,7 @@ describe API::Runners, api: true do
it "should return runner's details" do
get api("/runners/#{shared_runner.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
end
end
@@ -128,7 +128,7 @@ describe API::Runners, api: true do
it "should return runner's details" do
get api("/runners/#{specific_runner.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['description']).to eq(specific_runner.description)
end
end
@@ -136,7 +136,7 @@ describe API::Runners, api: true do
it 'should return 404 if runner does not exists' do
get api('/runners/9999', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -145,7 +145,7 @@ describe API::Runners, api: true do
it "should return runner's details" do
get api("/runners/#{specific_runner.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['description']).to eq(specific_runner.description)
end
end
@@ -154,7 +154,7 @@ describe API::Runners, api: true do
it "should return runner's details" do
get api("/runners/#{shared_runner.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
end
end
@@ -164,7 +164,7 @@ describe API::Runners, api: true do
it "should not return runner's details" do
get api("/runners/#{specific_runner.id}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -172,7 +172,7 @@ describe API::Runners, api: true do
it "should not return runner's details" do
get api("/runners/#{specific_runner.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -184,33 +184,42 @@ describe API::Runners, api: true 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']
+ update_runner(shared_runner.id, admin, description: "#{description}_updated",
+ active: !active,
+ tag_list: ['ruby2.1', 'pgsql', 'mysql'],
+ run_untagged: 'false',
+ locked: 'true')
shared_runner.reload
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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')
+ expect(shared_runner.run_untagged?).to be(false)
+ expect(shared_runner.locked?).to be(true)
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'
+ update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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'
+ update_runner(9999, admin, description: 'test')
+
+ expect(response).to have_http_status(404)
+ end
- expect(response.status).to eq(404)
+ def update_runner(id, user, args)
+ put api("/runners/#{id}", user), args
end
end
@@ -219,7 +228,7 @@ describe API::Runners, api: true do
it 'should not update runner' do
put api("/runners/#{shared_runner.id}", user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -227,7 +236,7 @@ describe API::Runners, api: true do
it 'should not update runner without access to it' do
put api("/runners/#{specific_runner.id}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should update runner with access to it' do
@@ -235,7 +244,7 @@ describe API::Runners, api: true do
put api("/runners/#{specific_runner.id}", admin), description: 'test'
specific_runner.reload
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(specific_runner.description).to eq('test')
expect(specific_runner.description).not_to eq(description)
end
@@ -246,7 +255,7 @@ describe API::Runners, api: true do
it 'should not delete runner' do
put api("/runners/#{specific_runner.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -258,7 +267,7 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(200)
end
end
@@ -267,21 +276,21 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(200)
end
end
it 'should return 404 if runner does not exists' do
delete api('/runners/9999', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -289,26 +298,26 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(200)
end
end
end
@@ -317,7 +326,7 @@ describe API::Runners, api: true do
it 'should not delete runner' do
delete api("/runners/#{specific_runner.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -328,7 +337,7 @@ describe API::Runners, api: true do
get api("/projects/#{project.id}/runners", user)
shared = json_response.any?{ |r| r['is_shared'] }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -338,7 +347,7 @@ describe API::Runners, api: true do
it "should not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -346,35 +355,47 @@ describe API::Runners, api: true do
it "should not return project's runners" do
get api("/projects/#{project.id}/runners")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(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|
+ let(:specific_runner2) do
+ create(:ci_runner).tap do |runner|
create(:ci_runner_project, runner: runner, project: project2)
end
+ end
+ it 'should enable specific runner' do
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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(409)
+ end
+
+ it 'should not enable locked runner' do
+ specific_runner2.update(locked: true)
+
+ expect do
+ post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ end.to change{ project.runners.count }.by(0)
+
+ expect(response).to have_http_status(403)
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)
+ expect(response).to have_http_status(403)
end
context 'user is admin' do
@@ -382,7 +403,7 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(201)
end
end
@@ -390,14 +411,14 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(400)
end
end
@@ -405,7 +426,7 @@ describe API::Runners, api: true do
it 'should not enable runner' do
post api("/projects/#{project.id}/runners", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -413,7 +434,7 @@ describe API::Runners, api: true do
it 'should not enable runner' do
post api("/projects/#{project.id}/runners")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -425,7 +446,7 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(200)
end
end
@@ -434,14 +455,14 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
@@ -449,7 +470,7 @@ describe API::Runners, api: true 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)
+ expect(response).to have_http_status(403)
end
end
@@ -457,7 +478,7 @@ describe API::Runners, api: true do
it "should not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index fed9ae1949b..a2446e12804 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -14,7 +14,7 @@ describe API::API, api: true do
it "should update #{service} settings" do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return if required fields missing" do
@@ -45,7 +45,7 @@ describe API::API, api: true do
it "should delete #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
@@ -64,20 +64,20 @@ describe API::API, api: true do
it 'should return authentication error when unauthenticated' do
get api("/projects/#{project.id}/services/#{dashed_service}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should return all properties of service #{service} when authenticated as admin" do
get api("/projects/#{project.id}/services/#{dashed_service}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
end
it "should return properties of service #{service} other than passwords when authenticated as project owner" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
end
@@ -85,9 +85,8 @@ describe API::API, api: true do
project.team << [user2, :developer]
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
-
end
end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index fbd57b34a58..c15b7ff9792 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
context "when valid password" do
it "should return private token" do
post api("/session"), email: user.email, password: '12345678'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['email']).to eq(user.email)
expect(json_response['private_token']).to eq(user.private_token)
@@ -48,7 +48,7 @@ describe API::API, api: true do
context "when invalid password" do
it "should return authentication error" do
post api("/session"), email: user.email, password: '123'
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
expect(json_response['email']).to be_nil
expect(json_response['private_token']).to be_nil
@@ -58,7 +58,7 @@ describe API::API, api: true do
context "when empty password" do
it "should return authentication error" do
post api("/session"), email: user.email
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
expect(json_response['email']).to be_nil
expect(json_response['private_token']).to be_nil
@@ -68,7 +68,7 @@ describe API::API, api: true do
context "when empty name" do
it "should return authentication error" do
post api("/session"), password: user.password
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
expect(json_response['email']).to be_nil
expect(json_response['private_token']).to be_nil
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index c815a8e1d73..684c2cd8e24 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -6,24 +6,30 @@ describe API::API, 'Settings', api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
-
describe "GET /application/settings" do
it "should return application settings" do
get api("/application/settings", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Hash
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['signin_enabled']).to be_truthy
+ expect(json_response['repository_storage']).to eq('default')
end
end
describe "PUT /application/settings" do
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
it "should update application settings" do
put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false
- expect(response.status).to eq(200)
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
+ expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
+ expect(json_response['repository_storage']).to eq('custom')
end
end
end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
new file mode 100644
index 00000000000..28067f8ca88
--- /dev/null
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe API::SidekiqMetrics, api: true do
+ include ApiHelpers
+
+ let(:admin) { create(:user, :admin) }
+
+ describe 'GET sidekiq/*' do
+ it 'defines the `queue_metrics` endpoint' do
+ get api('/sidekiq/queue_metrics', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ end
+
+ it 'defines the `process_metrics` endpoint' do
+ get api('/sidekiq/process_metrics', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['processes']).to be_an Array
+ end
+
+ it 'defines the `job_stats` endpoint' do
+ get api('/sidekiq/job_stats', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ end
+
+ it 'defines the `compound_metrics` endpoint' do
+ get api('/sidekiq/compound_metrics', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a Hash
+ expect(json_response['queues']).to be_a Hash
+ expect(json_response['processes']).to be_an Array
+ expect(json_response['jobs']).to be_a Hash
+ end
+ end
+end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 3e676515488..cf66f261ade 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -13,21 +13,21 @@ describe API::API, api: true do
context "when no user" do
it "should return authentication error" do
get api("/hooks")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when not an admin" do
it "should return forbidden error" do
get api("/hooks", user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
it "should return an array of hooks" do
get api("/hooks", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
end
@@ -43,26 +43,26 @@ describe API::API, api: true do
it "should respond with 400 if url not given" do
post api("/hooks", admin)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it "should not create new hook without url" do
expect do
post api("/hooks", admin)
- end.to_not change { SystemHook.count }
+ end.not_to change { SystemHook.count }
end
end
describe "GET /hooks/:id" do
it "should return hook by id" do
get api("/hooks/#{hook.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['event_name']).to eq('project_create')
end
it "should return 404 on failure" do
get api("/hooks/404", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -75,7 +75,7 @@ describe API::API, api: true do
it "should return success if hook id not found" do
delete api("/hooks/12345", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 12e170b232f..fa700ab7343 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
context 'without releases' do
it "should return an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
end
@@ -33,7 +33,7 @@ describe API::API, api: true do
it "should return an array of project tags with release info" do
get api("/projects/#{project.id}/repository/tags", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
expect(json_response.first['message']).to eq('Version 1.1.0')
@@ -48,14 +48,14 @@ describe API::API, api: true do
it 'returns a specific tag' do
get api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(tag_name)
end
it 'returns 404 for an invalid tag name' do
get api("/projects/#{project.id}/repository/tags/foobar", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -66,7 +66,7 @@ describe API::API, api: true do
tag_name: 'v7.0.1',
ref: 'master'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq('v7.0.1')
end
end
@@ -78,7 +78,7 @@ describe API::API, api: true do
ref: 'master',
release_description: 'Wow'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq('v7.0.1')
expect(json_response['release']['description']).to eq('Wow')
end
@@ -94,13 +94,13 @@ describe API::API, api: true do
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(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
end
@@ -117,7 +117,7 @@ describe API::API, api: true do
ref: 'master',
message: 'Release 7.1.0'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['name']).to eq('v7.1.0')
expect(json_response['message']).to eq('Release 7.1.0')
end
@@ -127,14 +127,14 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags", user2),
tag_name: 'v1.9.0',
ref: '621491c677087aa243f165eab467bfdfbee00be1'
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it 'should return 400 if tag name is invalid' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v 1.0.0',
ref: 'master'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Tag name invalid')
end
@@ -142,11 +142,11 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v8.0.0',
ref: 'master'
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v8.0.0',
ref: 'master'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Tag v8.0.0 already exists')
end
@@ -154,7 +154,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'mytag',
ref: 'foo'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Target foo is invalid')
end
end
@@ -167,7 +167,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['tag_name']).to eq(tag_name)
expect(json_response['description']).to eq(description)
end
@@ -176,7 +176,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: description
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('Tag does not exist')
end
@@ -190,7 +190,7 @@ describe API::API, api: true do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Release already exists')
end
end
@@ -211,7 +211,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['tag_name']).to eq(tag_name)
expect(json_response['description']).to eq(new_description)
end
@@ -221,7 +221,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: new_description
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('Tag does not exist')
end
@@ -229,7 +229,7 @@ describe API::API, api: true do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('Release does not exist')
end
end
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
new file mode 100644
index 00000000000..68d0f41b489
--- /dev/null
+++ b/spec/requests/api/templates_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe API::Templates, api: true do
+ include ApiHelpers
+
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
+
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ describe 'the TemplateList Entity' do
+ before { get api('/gitignores') }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
+ end
+ end
+
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
+ end
+ end
+
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
+
+ expect(response).to have_http_status(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
new file mode 100644
index 00000000000..92a4fa216cd
--- /dev/null
+++ b/spec/requests/api/todos_spec.rb
@@ -0,0 +1,190 @@
+require 'spec_helper'
+
+describe API::Todos, api: true do
+ include ApiHelpers
+
+ let(:project_1) { create(:project) }
+ let(:project_2) { create(:project) }
+ let(:author_1) { create(:user) }
+ let(:author_2) { create(:user) }
+ let(:john_doe) { create(:user, username: 'john_doe') }
+ let(:merge_request) { create(:merge_request, source_project: project_1) }
+ let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
+ let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
+ let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
+ let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
+
+ before do
+ project_1.team << [john_doe, :developer]
+ project_2.team << [john_doe, :developer]
+ end
+
+ describe 'GET /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns an array of pending todos for current user' do
+ get api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response[0]['id']).to eq(pending_3.id)
+ expect(json_response[0]['project']).to be_a Hash
+ expect(json_response[0]['author']).to be_a Hash
+ expect(json_response[0]['target_type']).to be_present
+ expect(json_response[0]['target']).to be_a Hash
+ expect(json_response[0]['target_url']).to be_present
+ expect(json_response[0]['body']).to be_present
+ expect(json_response[0]['state']).to eq('pending')
+ expect(json_response[0]['action_name']).to eq('assigned')
+ expect(json_response[0]['created_at']).to be_present
+ end
+
+ context 'and using the author filter' do
+ it 'filters based on author_id param' do
+ get api('/todos', john_doe), { author_id: author_2.id }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+ end
+
+ context 'and using the type filter' do
+ it 'filters based on type param' do
+ create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request)
+
+ get api('/todos', john_doe), { type: 'MergeRequest' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the state filter' do
+ it 'filters based on state param' do
+ get api('/todos', john_doe), { state: 'done' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the project filter' do
+ it 'filters based on project_id param' do
+ get api('/todos', john_doe), { project_id: project_2.id }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the action filter' do
+ it 'filters based on action param' do
+ get api('/todos', john_doe), { action: 'mentioned' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /todos/:id' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api("/todos/#{pending_1.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks a todo as done' do
+ delete api("/todos/#{pending_1.id}", john_doe)
+
+ expect(response.status).to eq(200)
+ expect(pending_1.reload).to be_done
+ end
+ end
+ end
+
+ describe 'DELETE /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks all todos as done' do
+ delete api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(pending_1.reload).to be_done
+ expect(pending_2.reload).to be_done
+ expect(pending_3.reload).to be_done
+ end
+ end
+ end
+
+ shared_examples 'an issuable' do |issuable_type|
+ it 'creates a todo on an issuable' do
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+
+ expect(response.status).to eq(201)
+ expect(json_response['project']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_type']).to eq(issuable.class.name)
+ expect(json_response['target']).to be_a Hash
+ expect(json_response['target_url']).to be_present
+ expect(json_response['body']).to be_present
+ expect(json_response['state']).to eq('pending')
+ expect(json_response['action_name']).to eq('marked')
+ expect(json_response['created_at']).to be_present
+ end
+
+ it 'returns 304 there already exist a todo on that issuable' do
+ create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
+
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the issuable is not found' do
+ post api("/projects/#{project_1.id}/#{issuable_type}/123/todo", john_doe)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'POST :id/issuable_type/:issueable_id/todo' do
+ context 'for an issue' do
+ it_behaves_like 'an issuable', 'issues' do
+ let(:issuable) { create(:issue, author: author_1, project: project_1) }
+ end
+ end
+
+ context 'for a merge request' do
+ it_behaves_like 'an issuable', 'merge_requests' do
+ let(:issuable) { merge_request }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 0510b77a39b..8992996c30a 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -23,39 +23,39 @@ describe API::API do
end
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
context 'Handles errors' do
it 'should return bad request if token is missing' do
post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return not found if project is not found' do
post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should return unauthorized if token is for different project' do
post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'Have a commit' do
- let(:commit) { project.ci_commits.last }
+ let(:pipeline) { project.pipelines.last }
it 'should create builds' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
- expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.size).to eq(2)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No builds created')
end
@@ -66,21 +66,21 @@ describe API::API do
it 'should validate variables to be a hash' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash')
end
it 'should validate variables needs to be a map of key-valued strings' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
it 'create trigger request with variables' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
- expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.first.trigger_request.variables).to eq(variables)
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
end
end
end
@@ -91,7 +91,7 @@ describe API::API do
it 'should return list of triggers' do
get api("/projects/#{project.id}/triggers", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_a(Array)
expect(json_response[0]).to have_key('token')
end
@@ -101,7 +101,7 @@ describe API::API do
it 'should not return triggers list' do
get api("/projects/#{project.id}/triggers", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -109,7 +109,7 @@ describe API::API do
it 'should not return triggers list' do
get api("/projects/#{project.id}/triggers")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -119,14 +119,14 @@ describe API::API do
it 'should return trigger details' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
@@ -134,7 +134,7 @@ describe API::API do
it 'should not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -142,7 +142,7 @@ describe API::API do
it 'should not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -154,7 +154,7 @@ describe API::API do
post api("/projects/#{project.id}/triggers", user)
end.to change{project.triggers.count}.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response).to be_a(Hash)
end
end
@@ -163,7 +163,7 @@ describe API::API do
it 'should not create trigger' do
post api("/projects/#{project.id}/triggers", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -171,7 +171,7 @@ describe API::API do
it 'should not create trigger' do
post api("/projects/#{project.id}/triggers")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -182,13 +182,13 @@ describe API::API 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
@@ -196,7 +196,7 @@ describe API::API do
it 'should not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -204,7 +204,7 @@ describe API::API do
it 'should not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 40b24c125b5..e43e3e269bf 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -15,12 +15,12 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/users")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- #These specs are written just in case API authentication is not required anymore
+ # These specs are written just in case API authentication is not required anymore
context "when public level is restricted" do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -29,18 +29,18 @@ describe API::API, api: true do
it "renders 403" do
get api("/users")
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "renders 404" do
get api("/users/#{user.id}")
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
it "should return an array of users" do
get api("/users", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
@@ -50,7 +50,7 @@ describe API::API, api: true do
it "should return one user" do
get api("/users?username=#{omniauth_user.username}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -59,7 +59,7 @@ describe API::API, api: true do
context "when admin" do
it "should return an array of users" do
get api("/users", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
expect(json_response.first.keys).to include 'identities'
@@ -74,24 +74,24 @@ describe API::API, api: true do
describe "GET /users/:id" do
it "should return a user by id" do
get api("/users/#{user.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
end
it "should return a 401 if unauthenticated" do
get api("/users/9998")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should return a 404 error if user id not found" do
get api("/users/9999", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
it "should return a 404 if invalid ID" do
get api("/users/1ASDF", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -106,7 +106,7 @@ describe API::API, api: true do
it "should create user with correct attributes" do
post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -116,7 +116,7 @@ describe API::API, api: true do
it "should create non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -126,7 +126,7 @@ describe API::API, api: true do
it "should create non-admin users by default" do
post api('/users', admin), attributes_for(:user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -135,12 +135,12 @@ describe API::API, api: true do
it "should return 201 Created on success" do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it 'creates non-external users by default' do
post api("/users", admin), attributes_for(:user)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -150,7 +150,7 @@ describe API::API, api: true do
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)
+ expect(response).to have_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -163,27 +163,27 @@ describe API::API, api: true do
email: 'invalid email',
password: 'password',
name: 'test'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 error if name not given' do
post api('/users', admin), attributes_for(:user).except(:name)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 error if password not given' do
post api('/users', admin), attributes_for(:user).except(:password)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 error if email not given' do
post api('/users', admin), attributes_for(:user).except(:email)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 error if username not given' do
post api('/users', admin), attributes_for(:user).except(:username)
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return 400 error if user does not validate' do
@@ -194,7 +194,7 @@ describe API::API, api: true do
name: 'test',
bio: 'g' * 256,
projects_limit: -1
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['password']).
to eq(['is too short (minimum is 8 characters)'])
expect(json_response['message']['bio']).
@@ -207,7 +207,7 @@ describe API::API, api: true do
it "shouldn't available for non admin users" do
post api("/users", user), attributes_for(:user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
context 'with existing user' do
@@ -227,7 +227,7 @@ describe API::API, api: true do
password: 'password',
username: 'foo'
end.to change { User.count }.by(0)
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Email has already been taken')
end
@@ -239,17 +239,16 @@ describe API::API, api: true do
password: 'password',
username: 'test'
end.to change { User.count }.by(0)
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Username has already been taken')
end
end
end
describe "GET /users/sign_up" do
-
it "should redirect to sign in page" do
get "/users/sign_up"
- expect(response.status).to eq(302)
+ expect(response).to have_http_status(302)
expect(response).to redirect_to(new_user_session_path)
end
end
@@ -261,41 +260,41 @@ describe API::API, api: true do
it "should update user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['bio']).to eq('new test bio')
expect(user.reload.bio).to eq('new test bio')
end
it 'should update user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email)
expect(user.reload.email).to eq(user.email)
end
it 'should update user with his own username' do
put api("/users/#{user.id}", admin), username: user.username
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(user.reload.username).to eq(user.username)
end
it "should update user's existing identity" do
put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
end
it 'should update user with new identity' do
put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(user.reload.identities.first.extern_uid).to eq('67890')
expect(user.reload.identities.first.provider).to eq('github')
end
it "should update admin status" do
put api("/users/#{user.id}", admin), { admin: true }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
expect(user.reload.admin).to eq(true)
end
@@ -309,7 +308,7 @@ describe API::API, api: true do
it "should not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
expect(admin_user.reload.admin).to eq(true)
expect(admin_user.can_create_group).to eq(false)
@@ -317,18 +316,18 @@ describe API::API, api: true do
it "should not allow invalid update" do
put api("/users/#{user.id}", admin), { email: 'invalid email' }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(user.reload.email).not_to eq('invalid email')
end
it "shouldn't available for non admin users" do
put api("/users/#{user.id}", user), attributes_for(:user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should return 404 for non-existing user" do
put api("/users/999999", admin), { bio: 'update should fail' }
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -344,7 +343,7 @@ describe API::API, api: true do
name: 'test',
bio: 'g' * 256,
projects_limit: -1
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']['password']).
to eq(['is too short (minimum is 8 characters)'])
expect(json_response['message']['bio']).
@@ -364,14 +363,14 @@ describe API::API, api: true do
it 'should return 409 conflict error if email address exists' do
put api("/users/#{@user.id}", admin), email: 'test@example.com'
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(@user.reload.email).to eq(@user.email)
end
it 'should return 409 conflict error if username taken' do
@user_id = User.all.last.id
put api("/users/#{@user.id}", admin), username: 'test'
- expect(response.status).to eq(409)
+ expect(response).to have_http_status(409)
expect(@user.reload.username).to eq(@user.username)
end
end
@@ -382,13 +381,13 @@ describe API::API, api: true do
it "should not create invalid ssh key" do
post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "key" not given')
end
it 'should not create key without title' do
post api("/users/#{user.id}/keys", admin), key: 'some key'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "title" not given')
end
@@ -401,7 +400,7 @@ describe API::API, api: true do
it "should return 405 for invalid ID" do
post api("/users/ASDF/keys", admin)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
end
end
@@ -411,14 +410,14 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api("/users/#{user.id}/keys")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'should return 404 for non-existing user' do
get api('/users/999999/keys', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -426,14 +425,14 @@ describe API::API, api: true do
user.keys << key
user.save
get api("/users/#{user.id}/keys", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
it "should return 405 for invalid ID" do
get api("/users/ASDF/keys", admin)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
end
end
end
@@ -444,7 +443,7 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
delete api("/users/#{user.id}/keys/42")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -455,20 +454,20 @@ describe API::API, api: true do
expect do
delete api("/users/#{user.id}/keys/#{key.id}", admin)
end.to change { user.keys.count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return 404 error if user not found' do
user.keys << key
user.save
delete api("/users/999999/keys/#{key.id}", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'should return 404 error if key not foud' do
delete api("/users/#{user.id}/keys/42", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
end
end
@@ -479,7 +478,7 @@ describe API::API, api: true do
it "should not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
end
@@ -492,7 +491,7 @@ describe API::API, api: true do
it "should raise error for invalid ID" do
post api("/users/ASDF/emails", admin)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
end
end
@@ -502,14 +501,14 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
get api("/users/#{user.id}/emails")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'should return 404 for non-existing user' do
get api('/users/999999/emails', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -517,14 +516,14 @@ describe API::API, api: true do
user.emails << email
user.save
get api("/users/#{user.id}/emails", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
it "should raise error for invalid ID" do
put api("/users/ASDF/emails", admin)
- expect(response.status).to eq(405)
+ expect(response).to have_http_status(405)
end
end
end
@@ -535,7 +534,7 @@ describe API::API, api: true do
context 'when unauthenticated' do
it 'should return authentication error' do
delete api("/users/#{user.id}/emails/42")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -546,20 +545,20 @@ describe API::API, api: true do
expect do
delete api("/users/#{user.id}/emails/#{email.id}", admin)
end.to change { user.emails.count }.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should return 404 error if user not found' do
user.emails << email
user.save
delete api("/users/999999/emails/#{email.id}", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'should return 404 error if email not foud' do
delete api("/users/#{user.id}/emails/42", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
@@ -574,24 +573,24 @@ describe API::API, api: true do
it "should delete user" do
delete api("/users/#{user.id}", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
expect(json_response['email']).to eq(user.email)
end
it "should not delete for unauthenticated user" do
delete api("/users/#{user.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "shouldn't available for non admin users" do
delete api("/users/#{user.id}", user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
it "should return 404 for non-existing user" do
delete api("/users/999999", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -603,7 +602,7 @@ describe API::API, api: true do
describe "GET /user" do
it "should return current user" do
get api("/user", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email)
expect(json_response['is_admin']).to eq(user.is_admin?)
expect(json_response['can_create_project']).to eq(user.can_create_project?)
@@ -613,7 +612,7 @@ describe API::API, api: true do
it "should return 401 error if user is unauthenticated" do
get api("/user")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -621,7 +620,7 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/user/keys")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -630,7 +629,7 @@ describe API::API, api: true do
user.keys << key
user.save
get api("/user/keys", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first["title"]).to eq(key.title)
end
@@ -642,13 +641,13 @@ describe API::API, api: true do
user.keys << key
user.save
get api("/user/keys/#{key.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["title"]).to eq(key.title)
end
it "should return 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -657,13 +656,13 @@ describe API::API, api: true do
user.save
admin
get api("/user/keys/#{key.id}", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
it "should return 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -673,29 +672,29 @@ describe API::API, api: true do
expect do
post api("/user/keys", user), key_attrs
end.to change{ user.keys.count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it "should return a 401 error if unauthorized" do
post api("/user/keys"), title: 'some title', key: 'some key'
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should not create ssh key without key" do
post api("/user/keys", user), title: 'title'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "key" not given')
end
it 'should not create ssh key without title' do
post api('/user/keys', user), key: 'some key'
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "title" not given')
end
it "should not create ssh key without title" do
post api("/user/keys", user), key: "somekey"
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
end
@@ -706,19 +705,19 @@ describe API::API, api: true do
expect do
delete api("/user/keys/#{key.id}", user)
end.to change{user.keys.count}.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return success if key ID not found" do
delete api("/user/keys/42", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return 401 error if unauthorized" do
user.keys << key
user.save
delete api("/user/keys/#{key.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should raise error for invalid ID" do
@@ -730,7 +729,7 @@ describe API::API, api: true do
context "when unauthenticated" do
it "should return authentication error" do
get api("/user/emails")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
@@ -739,7 +738,7 @@ describe API::API, api: true do
user.emails << email
user.save
get api("/user/emails", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first["email"]).to eq(email.email)
end
@@ -751,13 +750,13 @@ describe API::API, api: true do
user.emails << email
user.save
get api("/user/emails/#{email.id}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response["email"]).to eq(email.email)
end
it "should return 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -766,13 +765,13 @@ describe API::API, api: true do
user.save
admin
get api("/user/emails/#{email.id}", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
it "should return 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
@@ -782,17 +781,17 @@ describe API::API, api: true do
expect do
post api("/user/emails", user), email_attrs
end.to change{ user.emails.count }.by(1)
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
end
it "should return a 401 error if unauthorized" do
post api("/user/emails"), email: 'some email'
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should not create email with invalid email" do
post api("/user/emails", user), {}
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
end
end
@@ -804,19 +803,19 @@ describe API::API, api: true do
expect do
delete api("/user/emails/#{email.id}", user)
end.to change{user.emails.count}.by(-1)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return success if email ID not found" do
delete api("/user/emails/42", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it "should return 401 error if unauthorized" do
user.emails << email
user.save
delete api("/user/emails/#{email.id}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
it "should raise error for invalid ID" do
@@ -828,25 +827,25 @@ describe API::API, api: true do
before { admin }
it 'should block existing user' do
put api("/users/#{user.id}/block", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
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(response).to have_http_status(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)
+ expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'should return a 404 error if user id not found' do
put api('/users/9999/block', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
@@ -857,31 +856,31 @@ describe API::API, api: true do
it 'should unblock existing user' do
put api("/users/#{user.id}/unblock", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(user.reload.state).to eq('active')
end
it 'should unblock a blocked user' do
put api("/users/#{blocked_user.id}/unblock", admin)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
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(response).to have_http_status(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}/unblock", user)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'should return a 404 error if user id not found' do
put api('/users/9999/block', admin)
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index b1e1053d037..ddba18245f8 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -15,7 +15,7 @@ describe API::API, api: true do
it 'should return project variables' do
get api("/projects/#{project.id}/variables", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(json_response).to be_a(Array)
end
end
@@ -24,7 +24,7 @@ describe API::API, api: true do
it 'should not return project variables' do
get api("/projects/#{project.id}/variables", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -32,7 +32,7 @@ describe API::API, api: true do
it 'should not return project variables' do
get api("/projects/#{project.id}/variables")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -42,14 +42,14 @@ describe API::API, api: true do
it 'should return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
@@ -57,7 +57,7 @@ describe API::API, api: true do
it 'should not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -65,7 +65,7 @@ describe API::API, api: true do
it 'should not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -77,7 +77,7 @@ describe API::API, api: true 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(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
end
@@ -87,7 +87,7 @@ describe API::API, api: true 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)
+ expect(response).to have_http_status(400)
end
end
@@ -95,7 +95,7 @@ describe API::API, api: true do
it 'should not create variable' do
post api("/projects/#{project.id}/variables", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -103,7 +103,7 @@ describe API::API, api: true do
it 'should not create variable' do
post api("/projects/#{project.id}/variables")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -118,7 +118,7 @@ describe API::API, api: true do
updated_variable = project.variables.first
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
end
@@ -126,7 +126,7 @@ describe API::API, api: true do
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)
+ expect(response).to have_http_status(404)
end
end
@@ -134,7 +134,7 @@ describe API::API, api: true do
it 'should not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -142,7 +142,7 @@ describe API::API, api: true do
it 'should not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -153,13 +153,13 @@ describe API::API, api: true 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)
+ expect(response).to have_http_status(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)
+ expect(response).to have_http_status(404)
end
end
@@ -167,7 +167,7 @@ describe API::API, api: true do
it 'should not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
@@ -175,7 +175,7 @@ describe API::API, api: true do
it 'should not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index dfd361a2cdd..e7cbc3dd3a7 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -7,7 +7,7 @@ describe Ci::API::API do
let(:project) { FactoryGirl.create(:empty_project) }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe "Builds API for runners" do
@@ -20,13 +20,13 @@ describe Ci::API::API do
describe "POST /builds/register" do
it "should start a build" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
- build = commit.builds.first
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
+ build = pipeline.builds.first
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response['sha']).to eq(build.sha)
expect(runner.reload.platform).to eq("darwin")
end
@@ -34,45 +34,45 @@ describe Ci::API::API do
it "should return 404 error if no pending build found" do
post ci_api("/builds/register"), token: runner.token
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return 404 error if no builds for specific runner" do
- commit = FactoryGirl.create(:ci_commit, project: shared_project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
post ci_api("/builds/register"), token: runner.token
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "should return 404 error if no builds for shared runner" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
post ci_api("/builds/register"), token: shared_runner.token
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it "returns options" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
end
it "returns variables" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response["variables"]).to eq([
{ "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
{ "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
@@ -83,15 +83,15 @@ describe Ci::API::API do
it "returns variables for triggers" do
trigger = FactoryGirl.create(:ci_trigger, project: project)
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
- commit.create_builds(nil, trigger_request)
+ trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
+ pipeline.create_builds(nil, trigger_request)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response["variables"]).to eq([
{ "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
{ "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
@@ -103,13 +103,13 @@ describe Ci::API::API do
end
it "returns dependent builds" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil, nil)
- commit.builds.where(stage: 'test').each(&:success)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil, nil)
+ pipeline.builds.where(stage: 'test').each(&:success)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(json_response["depends_on_builds"].count).to eq(2)
expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
end
@@ -122,17 +122,49 @@ describe Ci::API::API do
it do
post ci_api("/builds/register"), token: runner.token, info: { param => value }
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
runner.reload
is_expected.to eq(value)
end
end
end
+
+ context 'when build has no tags' do
+ before do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, tags: [])
+ end
+
+ context 'when runner is allowed to pick untagged builds' do
+ before { runner.update_column(:run_untagged, true) }
+
+ it 'picks build' do
+ register_builds
+
+ expect(response).to have_http_status 201
+ end
+ end
+
+ context 'when runner is not allowed to pick untagged builds' do
+ before { runner.update_column(:run_untagged, false) }
+
+ it 'does not pick build' do
+ register_builds
+
+ expect(response).to have_http_status 404
+ end
+ end
+
+ def register_builds
+ post ci_api("/builds/register"), token: runner.token,
+ info: { platform: :darwin }
+ end
+ end
end
describe "PUT /builds/:id" do
- let(:commit) {create(:ci_commit, project: project)}
- let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) }
+ let(:pipeline) {create(:ci_pipeline, project: project)}
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
before do
build.run!
@@ -140,7 +172,7 @@ describe Ci::API::API do
end
it "should update a running build" do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
end
it 'should not override trace information when no trace is given' do
@@ -205,8 +237,8 @@ describe Ci::API::API do
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) { create(:ci_commit, project: project) }
- let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline, 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") }
@@ -220,14 +252,14 @@ describe Ci::API::API do
context "should authorize posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
- expect(response.status).to eq(200)
- expect(json_response["TempPath"]).to_not be_nil
+ expect(response).to have_http_status(200)
+ expect(json_response["TempPath"]).not_to be_nil
end
it "using token as header" do
post authorize_url, {}, headers_with_token
- expect(response.status).to eq(200)
- expect(json_response["TempPath"]).to_not be_nil
+ expect(response).to have_http_status(200)
+ expect(json_response["TempPath"]).not_to be_nil
end
end
@@ -235,13 +267,13 @@ describe Ci::API::API do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
post authorize_url, { token: build.token, filesize: 100 }, headers
- expect(response.status).to eq(413)
+ expect(response).to have_http_status(413)
end
it "using token as header" do
stub_application_setting(max_artifacts_size: 0)
post authorize_url, { filesize: 100 }, headers_with_token
- expect(response.status).to eq(413)
+ expect(response).to have_http_status(413)
end
end
@@ -249,7 +281,7 @@ describe Ci::API::API do
before { post authorize_url, { token: 'invalid', filesize: 100 } }
it 'should respond with forbidden' do
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(403)
end
end
end
@@ -261,33 +293,52 @@ describe Ci::API::API do
allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
end
- context 'build has been erased' do
+ describe 'build has been erased' do
let(:build) { create(:ci_build, erased_at: Time.now) }
- before { upload_artifacts(file_upload, headers_with_token) }
+
+ before do
+ upload_artifacts(file_upload, headers_with_token)
+ end
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)
- expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
+ describe 'uploading artifacts for a running build' do
+ shared_examples 'successful artifacts upload' do
+ it 'updates successfully' do
+ response_filename =
+ json_response['artifacts_file']['filename']
+
+ expect(response).to have_http_status(201)
+ expect(response_filename).to eq(file_upload.original_filename)
+ end
end
- it "uses accelerated file post" do
- upload_artifacts(file_upload, headers_with_token, true)
- expect(response.status).to eq(201)
- expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
+ context 'uses regular file post' do
+ before do
+ upload_artifacts(file_upload, headers_with_token, false)
+ end
+
+ it_behaves_like 'successful artifacts upload'
end
- it "updates artifact" do
- upload_artifacts(file_upload, headers_with_token)
- upload_artifacts(file_upload2, headers_with_token)
- expect(response.status).to eq(201)
- expect(json_response["artifacts_file"]["filename"]).to eq(file_upload2.original_filename)
+ context 'uses accelerated file post' do
+ before do
+ upload_artifacts(file_upload, headers_with_token, true)
+ end
+
+ it_behaves_like 'successful artifacts upload'
+ end
+
+ context 'updates artifact' do
+ before do
+ upload_artifacts(file_upload2, headers_with_token)
+ upload_artifacts(file_upload, headers_with_token)
+ end
+
+ it_behaves_like 'successful artifacts upload'
end
end
@@ -297,6 +348,7 @@ describe Ci::API::API do
let(:stored_artifacts_file) { build.reload.artifacts_file.file }
let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
+ let(:stored_artifacts_size) { build.reload.artifacts_size }
before do
post(post_url, post_data, headers_with_token)
@@ -311,9 +363,10 @@ describe Ci::API::API do
end
it 'stores artifacts and artifacts metadata' do
- expect(response.status).to eq(201)
+ expect(response).to have_http_status(201)
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+ expect(stored_artifacts_size).to eq(71759)
end
end
@@ -323,7 +376,7 @@ describe Ci::API::API do
end
it 'is expected to respond with bad request' do
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'does not store metadata' do
@@ -332,25 +385,61 @@ describe Ci::API::API do
end
end
+ context 'with an expire date' do
+ let!(:artifacts) { file_upload }
+
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'expire_in' => expire_in }
+ end
+
+ before do
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'with an expire_in given' do
+ let(:expire_in) { '7 days' }
+
+ it 'updates when specified' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at']).not_to be_empty
+ expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+ end
+ end
+
+ context 'with no expire_in given' do
+ let(:expire_in) { nil }
+
+ it 'ignores if not specified' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at']).to be_nil
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+ end
+
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)
+ expect(response).to have_http_status(413)
end
end
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)
+ expect(response).to have_http_status(400)
end
end
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)
+ expect(response).to have_http_status(403)
end
end
end
@@ -369,7 +458,7 @@ describe Ci::API::API 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)
+ expect(response).to have_http_status(400)
end
end
@@ -387,12 +476,17 @@ describe Ci::API::API do
describe 'DELETE /builds/:id/artifacts' do
let(:build) { create(:ci_build, :artifacts) }
- before { delete delete_url, token: build.token }
+
+ before do
+ delete delete_url, token: build.token
+ build.reload
+ end
it 'should remove build artifacts' do
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(200)
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_size).to be_nil
end
end
@@ -402,19 +496,19 @@ describe Ci::API::API do
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' }
+ { '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).to have_http_status(200)
expect(response.headers).to include download_headers
end
end
context 'build does not has artifacts' do
it 'should respond with not found' do
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index db8189ffb79..43596f07cb5 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -12,44 +12,85 @@ describe Ci::API::API do
end
describe "POST /runners/register" do
- describe "should create a runner if token provided" do
+ context 'when runner token is provided' do
before { post ci_api("/runners/register"), token: registration_token }
- it { expect(response.status).to eq(201) }
+ it 'creates runner with default values' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be true
+ end
end
- describe "should create a runner with description" do
- before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" }
+ context 'when runner description is provided' do
+ before do
+ post ci_api("/runners/register"), token: registration_token,
+ description: "server.hostname"
+ end
- it { expect(response.status).to eq(201) }
- it { expect(Ci::Runner.first.description).to eq("server.hostname") }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.description).to eq("server.hostname")
+ end
end
- describe "should create a runner with tags" do
- before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" }
+ context 'when runner tags are provided' do
+ before do
+ post ci_api("/runners/register"), token: registration_token,
+ tag_list: "tag1, tag2"
+ end
- it { expect(response.status).to eq(201) }
- it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
+ end
end
- describe "should create a runner if project token provided" do
+ context 'when option for running untagged jobs is provided' do
+ context 'when tags are provided' do
+ it 'creates runner' do
+ post ci_api("/runners/register"), token: registration_token,
+ run_untagged: false,
+ tag_list: ['tag']
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be false
+ end
+ end
+
+ context 'when tags are not provided' do
+ it 'does not create runner' do
+ post ci_api("/runners/register"), token: registration_token,
+ run_untagged: false
+
+ expect(response).to have_http_status 404
+ end
+ end
+ end
+
+ context 'when project token is provided' do
let(:project) { FactoryGirl.create(:empty_project) }
before { post ci_api("/runners/register"), token: project.runners_token }
- it { expect(response.status).to eq(201) }
- it { expect(project.runners.size).to eq(1) }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(project.runners.size).to eq(1)
+ end
end
- it "should return 403 error if token is invalid" do
- post ci_api("/runners/register"), token: 'invalid'
+ context 'when token is invalid' do
+ it 'returns 403 error' do
+ post ci_api("/runners/register"), token: 'invalid'
- expect(response.status).to eq(403)
+ expect(response).to have_http_status 403
+ end
end
- it "should return 400 error if no token" do
- post ci_api("/runners/register")
+ context 'when no token provided' do
+ it 'returns 400 error' do
+ post ci_api("/runners/register")
- expect(response.status).to eq(400)
+ expect(response).to have_http_status 400
+ end
end
%w(name version revision platform architecture).each do |param|
@@ -60,7 +101,7 @@ describe Ci::API::API do
it do
post ci_api("/runners/register"), token: registration_token, info: { param => value }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status 201
is_expected.to eq(value)
end
end
@@ -71,7 +112,7 @@ describe Ci::API::API do
let!(:runner) { FactoryGirl.create(:ci_runner) }
before { delete ci_api("/runners/delete"), token: runner.token }
- it { expect(response.status).to eq(200) }
+ it { expect(response).to have_http_status 200 }
it { expect(Ci::Runner.count).to eq(0) }
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 0ef03f9371b..f12678e5a8e 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -15,39 +15,39 @@ describe Ci::API::API do
end
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
context 'Handles errors' do
it 'should return bad request if token is missing' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
end
it 'should return not found if project is not found' do
post ci_api('/projects/0/refs/master/trigger'), options
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(404)
end
it 'should return unauthorized if token is for different project' do
post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'Have a commit' do
- let(:commit) { project.ci_commits.last }
+ let(:pipeline) { project.pipelines.last }
it 'should create builds' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
- expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.size).to eq(2)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No builds created')
end
@@ -58,21 +58,21 @@ describe Ci::API::API do
it 'should validate variables to be a hash' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash')
end
it 'should validate variables needs to be a map of key-valued strings' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
it 'create trigger request with variables' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
- expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.first.trigger_request.variables).to eq(variables)
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
end
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
new file mode 100644
index 00000000000..82ab582beac
--- /dev/null
+++ b/spec/requests/git_http_spec.rb
@@ -0,0 +1,398 @@
+require "spec_helper"
+
+describe 'Git HTTP requests', lib: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, path: 'project.git-project') }
+
+ it "gives WWW-Authenticate hints" do
+ clone_get('doesnt/exist.git')
+
+ expect(response.header['WWW-Authenticate']).to start_with('Basic ')
+ end
+
+ context "when the project doesn't exist" do
+ context "when no authentication is provided" do
+ it "responds with status 401 (no project existence information leak)" do
+ download('doesnt/exist.git') do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context "when username and password are provided" do
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context "when authentication succeeds" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+ end
+
+ context "when the Wiki for a project exists" do
+ it "responds with the right project" do
+ wiki = ProjectWiki.new(project)
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+
+ download("/#{wiki.repository.path_with_namespace}.git") do |response|
+ json_body = ActiveSupport::JSON.decode(response.body)
+
+ expect(response).to have_http_status(200)
+ expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+ end
+ end
+ end
+
+ context "when the project exists" do
+ let(:path) { "#{project.path_with_namespace}.git" }
+
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ it "downloads get status 200" do
+ download(path, {}) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ it "uploads get status 401" do
+ upload(path, {}) do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "with correct credentials" do
+ let(:env) { { user: user.username, password: user.password } }
+
+ it "uploads get status 200 (because Git hooks do the real check)" do
+ upload(path, env) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'but git-receive-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
+
+ upload(path, env) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ context 'but git-upload-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
+
+ download(path, {}) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context "when no authentication is provided" do
+ it "responds with status 401 to downloads" do
+ download(path, {}) do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ it "responds with status 401 to uploads" do
+ upload(path, {}) do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context "when username and password are provided" do
+ let(:env) { { user: user.username, password: 'nope' } }
+
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download(path, env) do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when the user is IP banned" do
+ it "responds with status 401" do
+ expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ clone_get(path, env)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context "when authentication succeeds" do
+ let(:env) { { user: user.username, password: user.password } }
+
+ context "when the user has access to the project" do
+ before do
+ project.team << [user, :master]
+ end
+
+ context "when the user is blocked" do
+ it "responds with status 404" do
+ user.block
+ project.team << [user, :master]
+
+ download(path, env) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "downloads get status 200" do
+ expect(Rack::Attack::Allow2Ban).to receive(:reset)
+
+ clone_get(path, env)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "uploads get status 200" do
+ upload(path, env) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ context "when an oauth token is provided" do
+ before do
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ end
+
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when blank password attempts follow a valid login" do
+ def attempt_login(include_password)
+ password = include_password ? user.password : ""
+ clone_get path, user: user.username, password: password
+ response.status
+ end
+
+ it "repeated attempts followed by successful attempt" do
+ options = Gitlab.config.rack_attack.git_basic_auth
+ maxretry = options[:maxretry] - 1
+ ip = '1.2.3.4'
+
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Rack::Attack::Allow2Ban.reset(ip, options)
+
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
+
+ expect(attempt_login(true)).to eq(200)
+ expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
+
+ Rack::Attack::Allow2Ban.reset(ip, options)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "downloads get status 404" do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ it "uploads get status 200 (because Git hooks do the real check)" do
+ upload(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+ end
+
+ context "when a gitlab ci token is provided" do
+ let(:token) { 123 }
+ let(:project) { FactoryGirl.create :empty_project }
+
+ before do
+ project.update_attributes(runners_token: token, builds_enabled: true)
+ end
+
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+ end
+
+ context "when the project path doesn't end in .git" do
+ context "GET info/refs" do
+ let(:path) { "/#{project.path_with_namespace}/info/refs" }
+
+ context "when no params are added" do
+ before { get path }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ end
+ end
+
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+ before { get path, params }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+ before { get path, params }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
+ before { get path, params }
+
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+
+ context "POST git-receive-pack" do
+ it "failes to find a route" do
+ expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+ end
+
+ context "retrieving an info/refs file" do
+ before { project.update_attribute(:visibility_level, Project::PUBLIC) }
+
+ context "when the file exists" do
+ before do
+ # Provide a dummy file in its place
+ allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
+ allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
+ end
+
+ get "/#{project.path_with_namespace}/blob/master/info/refs"
+ end
+
+ it "returns the file" do
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context "when the file does not exist" do
+ before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
+
+ it "returns not found" do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ def clone_get(project, options={})
+ get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def clone_post(project, options={})
+ post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_get(project, options={})
+ get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_post(project, options={})
+ post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def download(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ clone_get(*args)
+ yield response
+
+ clone_post(*args)
+ yield response
+ end
+
+ def upload(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ push_get(*args)
+ yield response
+
+ push_post(*args)
+ yield response
+ end
+
+ def auth_env(user, password, spnego_request_token)
+ env = {}
+ if user && password
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
+ elsif spnego_request_token
+ env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ end
+
+ env
+ end
+end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
new file mode 100644
index 00000000000..c6172b9cc7d
--- /dev/null
+++ b/spec/requests/jwt_controller_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe JwtController do
+ let(:service) { double(execute: {}) }
+ let(:service_class) { double(new: service) }
+ let(:service_name) { 'test' }
+ let(:parameters) { { service: service_name } }
+
+ before { stub_const('JwtController::SERVICES', service_name => service_class) }
+
+ context 'existing service' do
+ subject! { get '/jwt/auth', parameters }
+
+ it { expect(response).to have_http_status(200) }
+
+ context 'returning custom http code' do
+ let(:service) { double(execute: { http_status: 505 }) }
+
+ it { expect(response).to have_http_status(505) }
+ end
+ end
+
+ context 'when using authorized request' do
+ context 'using CI token' do
+ let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
+
+ subject! { get '/jwt/auth', parameters, headers }
+
+ context 'project with enabled CI' do
+ let(:builds_enabled) { true }
+
+ it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
+ end
+
+ context 'project with disabled CI' do
+ let(:builds_enabled) { false }
+
+ it { expect(response).to have_http_status(403) }
+ end
+ end
+
+ context 'using User login' do
+ let(:user) { create(:user) }
+ let(:headers) { { authorization: credentials('user', 'password') } }
+
+ before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
+
+ subject! { get '/jwt/auth', parameters, headers }
+
+ it { expect(service_class).to have_received(:new).with(nil, user, parameters) }
+ end
+
+ context 'using invalid login' do
+ let(:headers) { { authorization: credentials('invalid', 'password') } }
+
+ subject! { get '/jwt/auth', parameters, headers }
+
+ it { expect(response).to have_http_status(403) }
+ end
+ end
+
+ context 'unknown service' do
+ subject! { get '/jwt/auth', service: 'unknown' }
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ def credentials(login, password)
+ ActionController::HttpAuthentication::Basic.encode_credentials(login, password)
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index cd16a8e6322..8b19936ae6d 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -95,7 +95,6 @@ describe Admin::HooksController, "routing" do
it "to #destroy" do
expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1')
end
-
end
# admin_logs GET /admin/logs(.:format) admin/logs#show
@@ -118,3 +117,10 @@ describe Admin::DashboardController, "routing" do
expect(get("/admin")).to route_to('admin/dashboard#index')
end
end
+
+# admin_health_check GET /admin/health_check(.:format) admin/health_check#show
+describe Admin::HealthCheckController, "routing" do
+ it "to #show" do
+ expect(get("/admin/health_check")).to route_to('admin/health_check#show')
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 538f44e4f3f..620f328a114 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -165,7 +165,6 @@ describe Projects::TagsController, 'routing' do
end
end
-
# project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index
# POST /:project_id/deploy_keys(.:format) deploy_keys#create
# new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1527eddfa48..8a8e131c57b 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -1,5 +1,42 @@
require 'spec_helper'
+# user GET /u/:username/
+# user_groups GET /u/:username/groups(.:format)
+# user_projects GET /u/:username/projects(.:format)
+# user_contributed_projects GET /u/:username/contributed(.:format)
+# user_snippets GET /u/:username/snippets(.:format)
+# user_calendar GET /u/:username/calendar(.:format)
+# user_calendar_activities GET /u/:username/calendar_activities(.:format)
+describe UsersController, "routing" do
+ it "to #show" do
+ expect(get("/u/User")).to route_to('users#show', username: 'User')
+ end
+
+ it "to #groups" do
+ expect(get("/u/User/groups")).to route_to('users#groups', username: 'User')
+ end
+
+ it "to #projects" do
+ expect(get("/u/User/projects")).to route_to('users#projects', username: 'User')
+ end
+
+ it "to #contributed" do
+ expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User')
+ end
+
+ it "to #snippets" do
+ expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User')
+ end
+
+ it "to #calendar" do
+ expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User')
+ end
+
+ it "to #calendar_activities" do
+ expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
+ end
+end
+
# search GET /search(.:format) search#show
describe SearchController, "routing" do
it "to #show" do
@@ -27,10 +64,6 @@ end
# PUT /snippets/:id(.:format) snippets#update
# DELETE /snippets/:id(.:format) snippets#destroy
describe SnippetsController, "routing" do
- it "to #user_index" do
- expect(get("/s/User")).to route_to('snippets#index', username: 'User')
- end
-
it "to #raw" do
expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1')
end
@@ -220,7 +253,6 @@ describe RootController, 'routing' do
end
end
-
# new_user_session GET /users/sign_in(.:format) devise/sessions#new
# user_session POST /users/sign_in(.:format) devise/sessions#create
# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
@@ -243,3 +275,13 @@ describe "Groups", "routing" do
expect(get('/1')).to route_to('namespaces#show', id: '1')
end
end
+
+describe HealthCheckController, 'routing' do
+ it 'to #index' do
+ expect(get('/health_check')).to route_to('health_check#index')
+ end
+
+ it 'also supports passing checks in the url' do
+ expect(get('/health_check/email')).to route_to('health_check#index', checks: 'email')
+ end
+end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
new file mode 100644
index 00000000000..67777ad48bc
--- /dev/null
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Auth::ContainerRegistryAuthenticationService, services: true do
+ let(:current_project) { nil }
+ let(:current_user) { nil }
+ let(:current_params) { {} }
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
+ let(:payload) { JWT.decode(subject[:token], rsa_key).first }
+
+ subject { described_class.new(current_project, current_user, current_params).execute }
+
+ before do
+ allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
+ allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
+ end
+
+ shared_examples 'a valid token' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+
+ context 'a expirable' do
+ let(:expires_at) { Time.at(payload['exp']) }
+ let(:expire_delay) { 10 }
+
+ context 'for default configuration' do
+ it { expect(expires_at).not_to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ end
+
+ context 'for changed configuration' do
+ before { stub_application_setting(container_registry_token_expire_delay: expire_delay) }
+
+ it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ end
+ end
+ end
+
+ shared_examples 'a accessible' do
+ let(:access) do
+ [{
+ 'type' => 'repository',
+ 'name' => project.path_with_namespace,
+ 'actions' => actions,
+ }]
+ end
+
+ it_behaves_like 'a valid token'
+ it { expect(payload).to include('access' => access) }
+ end
+
+ shared_examples 'an inaccessible' do
+ it_behaves_like 'a valid token'
+ it { expect(payload).to include('access' => []) }
+ end
+
+ shared_examples 'a pullable' do
+ it_behaves_like 'a accessible' do
+ let(:actions) { ['pull'] }
+ end
+ end
+
+ shared_examples 'a pushable' do
+ it_behaves_like 'a accessible' do
+ let(:actions) { ['push'] }
+ end
+ end
+
+ shared_examples 'a pullable and pushable' do
+ it_behaves_like 'a accessible' do
+ let(:actions) { ['pull', 'push'] }
+ end
+ end
+
+ shared_examples 'a forbidden' do
+ it { is_expected.to include(http_status: 403) }
+ it { is_expected.not_to include(:token) }
+ end
+
+ describe '#full_access_token' do
+ let(:project) { create(:empty_project) }
+ let(:token) { described_class.full_access_token(project.path_with_namespace) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'a accessible' do
+ let(:actions) { ['*'] }
+ end
+ end
+
+ context 'user authorization' do
+ let(:project) { create(:project) }
+ let(:current_user) { create(:user) }
+
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
+
+ context 'allow developer to push images' do
+ before { project.team << [current_user, :developer] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ context 'allow reporter to pull images' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'return a least of privileges' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push,pull" }
+ end
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'disallow guest to pull or push images' do
+ before { project.team << [current_user, :guest] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
+ end
+
+ context 'project authorization' do
+ let(:current_project) { create(:empty_project) }
+
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
+
+ context 'allow to pull and push images' do
+ let(:current_params) do
+ { scope: "repository:#{current_project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'a pullable and pushable' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'for other projects' do
+ context 'when pulling' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ context 'allow for public' do
+ let(:project) { create(:empty_project, :public) }
+ it_behaves_like 'a pullable'
+ end
+
+ context 'disallow for private' do
+ let(:project) { create(:empty_project, :private) }
+ it_behaves_like 'an inaccessible'
+ end
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ context 'disallow for all' do
+ let(:project) { create(:empty_project, :public) }
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'for project without container registry' do
+ let(:project) { create(:empty_project, :public, container_registry_enabled: false) }
+
+ before { project.update(container_registry_enabled: false) }
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'unauthorized' do
+ context 'disallow to use scope-less authentication' do
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'for invalid scope' do
+ let(:current_params) do
+ { scope: 'invalid:aa:bb' }
+ end
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'for private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'for public project' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'when pulling and pushing' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'a forbidden'
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
index ecc3a88a262..8b0becd83d3 100644
--- a/spec/services/ci/create_builds_service_spec.rb
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreateBuildsService, services: true do
- let(:commit) { create(:ci_commit, ref: 'master') }
+ let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
describe '#execute' do
@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
- described_class.new(commit).execute(commit, nil, user, status)
+ described_class.new(pipeline).execute('test', user, status, nil)
end
context 'next builds available' do
@@ -17,6 +17,10 @@ describe Ci::CreateBuildsService, services: true do
it { is_expected.to be_an_instance_of Array }
it { is_expected.to all(be_an_instance_of Ci::Build) }
+
+ it 'does not persist created builds' do
+ expect(subject.first).not_to be_persisted
+ end
end
context 'builds skipped' do
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index dbdc5370bd8..ae4b7aca820 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -6,7 +6,7 @@ describe Ci::CreateTriggerRequestService, services: true do
let(:trigger) { create(:ci_trigger, project: project) }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe :execute do
@@ -27,8 +27,8 @@ describe Ci::CreateTriggerRequestService, services: true do
subject { service.execute(project, trigger, 'master') }
before do
- stub_ci_commit_yaml_file('{}')
- FactoryGirl.create :ci_commit, project: project
+ stub_ci_pipeline_yaml_file('{}')
+ FactoryGirl.create :ci_pipeline, project: project
end
it { expect(subject).to be_nil }
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 4cc4b3870d1..476a888e394 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,8 +5,8 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:commit) { project.ensure_ci_commit(commit_sha, 'master') }
- let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
+ let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
describe :execute do
before { build }
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index e81f9e757ac..f28f2f1438d 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -4,8 +4,8 @@ module Ci
describe RegisterBuildService, services: true do
let!(:service) { RegisterBuildService.new }
let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
- let!(:commit) { FactoryGirl.create :ci_commit, project: project }
- let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit }
+ let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
@@ -45,11 +45,73 @@ module Ci
end
end
+ context 'deleted projects' do
+ before do
+ project.update(pending_delete: true)
+ end
+
+ context 'for shared runners' do
+ before do
+ project.update(shared_runners_enabled: true)
+ end
+
+ it 'does not pick a build' do
+ expect(service.execute(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for specific runner' do
+ it 'does not pick a build' do
+ expect(service.execute(specific_runner)).to be_nil
+ end
+ end
+ end
+
context 'allow shared runners' do
before do
project.update(shared_runners_enabled: true)
end
+ context 'for multiple builds' do
+ let!(:project2) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_build }
+ let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 }
+
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(service.execute(shared_runner)).to eq(build1_project1)
+ expect(service.execute(shared_runner)).to eq(build1_project2)
+ expect(service.execute(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(service.execute(shared_runner)).to eq(build2_project1)
+ expect(service.execute(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(service.execute(shared_runner)).to eq(build3_project1)
+ end
+
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(service.execute(shared_runner)).to eq(build1_project1)
+ build1_project1.success
+ expect(service.execute(shared_runner)).to eq(build2_project1)
+
+ expect(service.execute(shared_runner)).to eq(build1_project2)
+ build1_project2.success
+ expect(service.execute(shared_runner)).to eq(build2_project2)
+ expect(service.execute(shared_runner)).to eq(build1_project3)
+ expect(service.execute(shared_runner)).to eq(build3_project1)
+ end
+ end
+
context 'shared runner' do
let(:build) { service.execute(shared_runner) }
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
index ea5dcfa068a..309213bd44c 100644
--- a/spec/services/create_commit_builds_service_spec.rb
+++ b/spec/services/create_commit_builds_service_spec.rb
@@ -6,12 +6,12 @@ describe CreateCommitBuildsService, services: true do
let(:user) { nil }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe :execute do
context 'valid params' do
- let(:commit) do
+ let(:pipeline) do
service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
@@ -20,11 +20,11 @@ describe CreateCommitBuildsService, services: true do
)
end
- it { expect(commit).to be_kind_of(Ci::Commit) }
- it { expect(commit).to be_valid }
- it { expect(commit).to be_persisted }
- it { expect(commit).to eq(project.ci_commits.last) }
- it { expect(commit.builds.first).to be_kind_of(Ci::Build) }
+ it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+ it { expect(pipeline).to be_valid }
+ it { expect(pipeline).to be_persisted }
+ it { expect(pipeline).to eq(project.pipelines.last) }
+ it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
end
context "skip tag if there is no build for it" do
@@ -39,8 +39,8 @@ describe CreateCommitBuildsService, services: true do
end
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
- config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
- stub_ci_commit_yaml_file(config)
+ config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
+ stub_ci_pipeline_yaml_file(config)
result = service.execute(project, user,
ref: 'refs/heads/0_1',
@@ -52,8 +52,8 @@ describe CreateCommitBuildsService, services: true do
end
end
- it 'skips creating ci_commit for refs without .gitlab-ci.yml' do
- stub_ci_commit_yaml_file(nil)
+ it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+ stub_ci_pipeline_yaml_file(nil)
result = service.execute(project, user,
ref: 'refs/heads/0_1',
before: '00000000',
@@ -61,115 +61,180 @@ describe CreateCommitBuildsService, services: true do
commits: [{ message: 'Message' }]
)
expect(result).to be_falsey
- expect(Ci::Commit.count).to eq(0)
+ expect(Ci::Pipeline.count).to eq(0)
end
it 'fails commits if yaml is invalid' do
message = 'message'
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
- stub_ci_commit_yaml_file('invalid: file: file')
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+ stub_ci_pipeline_yaml_file('invalid: file: file')
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq('failed')
- expect(commit.yaml_errors).to_not be_nil
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq('failed')
+ expect(pipeline.yaml_errors).not_to be_nil
end
- describe :ci_skip? do
+ context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
+ let(:messageFlip) { "some message[skip ci]" }
+ let(:capMessage) { "some message[CI SKIP]" }
+ let(:capMessageFlip) { "some message[SKIP CI]" }
before do
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
end
it "skips builds creation if there is [ci skip] tag in commit message" do
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq("skipped")
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
end
- it "does not skips builds creation if there is no [ci skip] tag in commit message" do
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" }
+ it "skips builds creation if there is [skip ci] tag in commit message" do
+ commits = [{ message: messageFlip }]
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
- commits = [{ message: "some message" }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
+ it "skips builds creation if there is [CI SKIP] tag in commit message" do
+ commits = [{ message: capMessage }]
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
- expect(commit).to be_persisted
- expect(commit.builds.first.name).to eq("staging")
+ it "skips builds creation if there is [SKIP CI] tag in commit message" do
+ commits = [{ message: capMessageFlip }]
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+ commits = [{ message: "some message" }]
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.first.name).to eq("staging")
end
it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
- stub_ci_commit_yaml_file('invalid: file: fiile')
+ stub_ci_pipeline_yaml_file('invalid: file: fiile')
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq("skipped")
- expect(commit.yaml_errors).to be_nil
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ expect(pipeline.yaml_errors).to be_nil
end
end
it "skips build creation if there are already builds" do
- allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml }
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
commits = [{ message: "message" }]
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.count(:all)).to eq(2)
+ pipeline = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.count(:all)).to eq(2)
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.count(:all)).to eq(2)
+ pipeline = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.count(:all)).to eq(2)
end
it "creates commit with failed status if yaml is invalid" do
- stub_ci_commit_yaml_file('invalid: file')
+ stub_ci_pipeline_yaml_file('invalid: file')
commits = [{ message: "some message" }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.status).to eq("failed")
+ expect(pipeline.builds.any?).to be false
+ end
- expect(commit).to be_persisted
- expect(commit.status).to eq("failed")
- expect(commit.builds.any?).to be false
+ context 'when there are no jobs for this pipeline' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_falsey
+ expect(Ci::Build.all).to be_empty
+ expect(Ci::Pipeline.count).to eq(0)
+ end
end
end
end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
new file mode 100644
index 00000000000..654e441f3cd
--- /dev/null
+++ b/spec/services/create_deployment_service_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe CreateDeploymentService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user, params) }
+
+ describe '#execute' do
+ let(:params) do
+ { environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ }
+ end
+
+ subject { service.execute }
+
+ context 'when no environments exist' do
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+
+ context 'when environment exist' do
+ before { create(:environment, project: project, name: 'production') }
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+
+ context 'for environment with invalid name' do
+ let(:params) do
+ { environment: 'name with spaces',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ }
+ end
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does not create a deployment' do
+ expect(subject).not_to be_persisted
+ end
+ end
+ end
+
+ describe 'processing of builds' do
+ let(:environment) { nil }
+
+ shared_examples 'does not create environment and deployment' do
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does not create a new deployment' do
+ expect { subject }.not_to change { Deployment.count }
+ end
+
+ it 'does not call a service' do
+ expect_any_instance_of(described_class).not_to receive(:execute)
+ subject
+ end
+ end
+
+ shared_examples 'does create environment and deployment' do
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+ end
+
+ it 'does create a new deployment' do
+ expect { subject }.to change { Deployment.count }.by(1)
+ end
+
+ it 'does call a service' do
+ expect_any_instance_of(described_class).to receive(:execute)
+ subject
+ end
+ end
+
+ context 'without environment specified' do
+ let(:build) { create(:ci_build, project: project) }
+
+ it_behaves_like 'does not create environment and deployment' do
+ subject { build.success }
+ end
+ end
+
+ context 'when environment is specified' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') }
+
+ context 'when build succeeds' do
+ it_behaves_like 'does create environment and deployment' do
+ subject { build.success }
+ end
+ end
+
+ context 'when build fails' do
+ it_behaves_like 'does not create environment and deployment' do
+ subject { build.drop }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/create_tag_service_spec.rb
index 91f9e663b66..7dc43c50b0d 100644
--- a/spec/services/create_tag_service_spec.rb
+++ b/spec/services/create_tag_service_spec.rb
@@ -41,12 +41,12 @@ describe CreateTagService, services: true do
it 'returns an error' do
expect(repository).to receive(:add_tag).
with(user, 'v1.1.0', 'master', 'Foo').
- and_raise(GitHooksService::PreReceiveError)
+ and_raise(GitHooksService::PreReceiveError, 'something went wrong')
response = service.execute('v1.1.0', 'master', 'Foo')
expect(response).to eq(status: :error,
- message: 'Tag creation was rejected by Git hook')
+ message: 'something went wrong')
end
end
end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index afa89b84175..eca8ddd8ea4 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -23,8 +23,8 @@ describe DestroyGroupService, services: true do
Sidekiq::Testing.inline! { destroy_group(group, user) }
end
- it { expect(gitlab_shell.exists?(group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(remove_path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
end
context 'Sidekiq fake' do
@@ -33,8 +33,8 @@ describe DestroyGroupService, services: true do
Sidekiq::Testing.fake! { destroy_group(group, user) }
end
- it { expect(gitlab_shell.exists?(group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(remove_path)).to be_truthy }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
end
end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 2bb9c3b3db3..3fc37a315c0 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -16,19 +16,18 @@ describe GitHooksService, services: true do
end
describe '#execute' do
-
context 'when receive hooks were successful' do
it 'should call post-receive hook' do
- hook = double(trigger: true)
+ hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true)
+ expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil])
end
end
context 'when pre-receive hook failed' do
it 'should not call post-receive hook' do
- expect(service).to receive(:run_hook).with('pre-receive').and_return(false)
+ expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
@@ -39,8 +38,8 @@ describe GitHooksService, services: true do
context 'when update hook failed' do
it 'should not call post-receive hook' do
- expect(service).to receive(:run_hook).with('pre-receive').and_return(true)
- expect(service).to receive(:run_hook).with('update').and_return(false)
+ expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
+ expect(service).to receive(:run_hook).with('update').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
@@ -48,6 +47,5 @@ describe GitHooksService, services: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
end
-
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index eeab540c2fd..afabeed4a80 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -14,7 +14,6 @@ describe GitPushService, services: true do
end
describe 'Push branches' do
-
let(:oldrev) { @oldrev }
let(:newrev) { @newrev }
@@ -23,7 +22,6 @@ describe GitPushService, services: true do
end
context 'new branch' do
-
let(:oldrev) { @blankrev }
it { is_expected.to be_truthy }
@@ -40,10 +38,21 @@ describe GitPushService, services: true do
subject
end
+
+ it 'flushes the branches cache' do
+ expect(project.repository).to receive(:expire_branches_cache)
+
+ subject
+ end
+
+ it 'flushes the branch count cache' do
+ expect(project.repository).to receive(:expire_branch_count_cache)
+
+ subject
+ end
end
context 'existing branch' do
-
it { is_expected.to be_truthy }
it 'flushes general cached data' do
@@ -52,10 +61,21 @@ describe GitPushService, services: true do
subject
end
+
+ it 'does not flush the branches cache' do
+ expect(project.repository).not_to receive(:expire_branches_cache)
+
+ subject
+ end
+
+ it 'does not flush the branch count cache' do
+ expect(project.repository).not_to receive(:expire_branch_count_cache)
+
+ subject
+ end
end
context 'rm branch' do
-
let(:newrev) { @blankrev }
it { is_expected.to be_truthy }
@@ -66,6 +86,18 @@ describe GitPushService, services: true do
subject
end
+ it 'flushes the branches cache' do
+ expect(project.repository).to receive(:expire_branches_cache)
+
+ subject
+ end
+
+ it 'flushes the branch count cache' do
+ expect(project.repository).to receive(:expire_branch_count_cache)
+
+ subject
+ end
+
it 'flushes general cached data' do
expect(project.repository).to receive(:expire_cache).
with('master', newrev)
@@ -158,49 +190,6 @@ describe GitPushService, services: true do
end
end
- describe "Updates main language" do
- context "before push" do
- it { expect(project.main_language).to eq(nil) }
- end
-
- context "after push" do
- def execute
- execute_service(project, user, @oldrev, @newrev, ref)
- end
-
- context "to master" do
- let(:ref) { @ref }
-
- context 'when main_language is nil' do
- it 'obtains the language from the repository' do
- expect(project.repository).to receive(:main_language)
- execute
- end
-
- it 'sets the project main language' do
- execute
- expect(project.main_language).to eq("Ruby")
- end
- end
-
- context 'when main_language is already set' do
- it 'does not check the repository' do
- execute # do an initial run to simulate lang being preset
- expect(project.repository).not_to receive(:main_language)
- execute
- end
- end
- end
-
- context "to other branch" do
- let(:ref) { 'refs/heads/feature/branch' }
-
- it { expect(project.main_language).to eq(nil) }
- end
- end
- end
-
-
describe "Updates git attributes" do
context "for default branch" do
it "calls the copy attributes method for the first push to the default branch" do
@@ -230,7 +219,6 @@ describe GitPushService, services: true do
end
end
-
describe "Webhooks" do
context "execute webhooks" do
it "when pushing a branch for the first time" do
@@ -355,7 +343,10 @@ describe GitPushService, services: true do
end
it "doesn't close issues when external issue tracker is in use" do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ allow_any_instance_of(Project).to receive(:default_issues_tracker?).
+ and_return(false)
+ external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid)
+ allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
expect do
@@ -495,7 +486,6 @@ describe GitPushService, services: true do
end
end
-
it 'increments the push counter' do
expect(housekeeping).to receive(:increment!)
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index a63656e6268..a4fcd44882d 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -11,6 +11,31 @@ describe GitTagPushService, services: true do
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
+ describe "Push tags" do
+ subject do
+ service.execute
+ service
+ end
+
+ it 'flushes general cached data' do
+ expect(project.repository).to receive(:expire_cache)
+
+ subject
+ end
+
+ it 'flushes the tags cache' do
+ expect(project.repository).to receive(:expire_tags_cache)
+
+ subject
+ end
+
+ it 'flushes the tag count cache' do
+ expect(project.repository).to receive(:expire_tag_count_cache)
+
+ subject
+ end
+ end
+
describe "Git Tag Push Data" do
before do
service.execute
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 6aefb48a4e8..71a0b8e2a12 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -13,8 +13,8 @@ describe Groups::CreateService, services: true do
end
context "cannot create group with restricted visibility level" do
- before { allow(current_application_settings).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
- it { is_expected.to_not be_persisted }
+ before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
+ it { is_expected.not_to be_persisted }
end
end
end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index e91906d0d49..4a689e64dc5 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -1,119 +1,265 @@
require 'spec_helper'
describe Issues::BulkUpdateService, services: true do
- let(:issue) { create(:issue, project: @project) }
-
- before do
- @user = create :user
- opts = {
- name: "GitLab",
- namespace: @user.namespace
- }
- @project = Projects::CreateService.new(@user, opts).execute
- end
+ let(:user) { create(:user) }
+ let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute }
- describe :close_issue do
+ let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute }
- before do
- @issues = 5.times.collect do
- create(:issue, project: @project)
- end
- @params = {
+ describe :close_issue do
+ let(:issues) { create_list(:issue, 5, project: project) }
+ let(:params) do
+ {
state_event: 'close',
- issues_ids: @issues.map(&:id)
+ issues_ids: issues.map(&:id).join(',')
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds and returns the correct number of issues updated' do
expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(@issues.count)
-
- expect(@project.issues.opened).to be_empty
- expect(@project.issues.closed).not_to be_empty
+ expect(result[:count]).to eq(issues.count)
end
+ it 'closes all the issues passed' do
+ expect(project.issues.opened).to be_empty
+ expect(project.issues.closed).not_to be_empty
+ end
end
describe :reopen_issues do
-
- before do
- @issues = 5.times.collect do
- create(:closed_issue, project: @project)
- end
- @params = {
+ let(:issues) { create_list(:closed_issue, 5, project: project) }
+ let(:params) do
+ {
state_event: 'reopen',
- issues_ids: @issues.map(&:id)
+ issues_ids: issues.map(&:id).join(',')
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds and returns the correct number of issues updated' do
expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(@issues.count)
-
- expect(@project.issues.closed).to be_empty
- expect(@project.issues.opened).not_to be_empty
+ expect(result[:count]).to eq(issues.count)
end
+ it 'reopens all the issues passed' do
+ expect(project.issues.closed).to be_empty
+ expect(project.issues.opened).not_to be_empty
+ end
end
- describe :update_assignee do
+ describe 'updating assignee' do
+ let(:issue) do
+ create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) }
+ end
- before do
- @new_assignee = create :user
- @params = {
- issues_ids: [issue.id],
- assignee_id: @new_assignee.id
+ let(:params) do
+ {
+ assignee_id: assignee_id,
+ issues_ids: issue.id.to_s
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(1)
+ context 'when the new assignee ID is a valid user' do
+ let(:new_assignee) { create(:user) }
+ let(:assignee_id) { new_assignee.id }
- expect(@project.issues.first.assignee).to eq(@new_assignee)
- end
+ it 'succeeds' do
+ expect(result[:success]).to be_truthy
+ expect(result[:count]).to eq(1)
+ end
- it 'allows mass-unassigning' do
- @project.issues.first.update_attribute(:assignee, @new_assignee)
- expect(@project.issues.first.assignee).not_to be_nil
+ it 'updates the assignee to the use ID passed' do
+ expect(issue.reload.assignee).to eq(new_assignee)
+ end
+ end
- @params[:assignee_id] = -1
+ context 'when the new assignee ID is -1' do
+ let(:assignee_id) { -1 }
- Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(@project.issues.first.assignee).to be_nil
+ it 'unassigns the issues' do
+ expect(issue.reload.assignee).to be_nil
+ end
end
- it 'does not unassign when assignee_id is not present' do
- @project.issues.first.update_attribute(:assignee, @new_assignee)
- expect(@project.issues.first.assignee).not_to be_nil
-
- @params[:assignee_id] = ''
+ context 'when the new assignee ID is not present' do
+ let(:assignee_id) { nil }
- Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(@project.issues.first.assignee).not_to be_nil
+ it 'does not unassign' do
+ expect(issue.reload.assignee).to eq(user)
+ end
end
end
- describe :update_milestone do
+ describe 'updating milestones' do
+ let(:issue) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project) }
- before do
- @milestone = create(:milestone, project: @project)
- @params = {
- issues_ids: [issue.id],
- milestone_id: @milestone.id
+ let(:params) do
+ {
+ issues_ids: issue.id.to_s,
+ milestone_id: milestone.id
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds' do
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
+ end
- expect(@project.issues.first.milestone).to eq(@milestone)
+ it 'updates the issue milestone' do
+ expect(project.issues.first.milestone).to eq(milestone)
end
end
+ describe 'updating labels' do
+ def create_issue_with_labels(labels)
+ create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) }
+ end
+
+ let(:bug) { create(:label, project: project) }
+ let(:regression) { create(:label, project: project) }
+ let(:merge_requests) { create(:label, project: project) }
+
+ let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) }
+ let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) }
+ let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) }
+ let(:issue_no_labels) { create(:issue, project: project) }
+ let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] }
+
+ let(:labels) { [] }
+ let(:add_labels) { [] }
+ let(:remove_labels) { [] }
+
+ let(:params) do
+ {
+ label_ids: labels.map(&:id),
+ add_label_ids: add_labels.map(&:id),
+ remove_label_ids: remove_labels.map(&:id),
+ issues_ids: issues.map(&:id).join(',')
+ }
+ end
+
+ context 'when label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_no_labels] }
+ let(:labels) { [bug, regression] }
+
+ it 'updates the labels of all issues passed to the labels passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id)))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+
+ context 'when those label IDs are empty' do
+ let(:labels) { [] }
+
+ it 'updates the issues passed to have no labels' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
+ end
+ end
+ end
+
+ context 'when add_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:add_labels) { [bug, regression, merge_requests] }
+
+ it 'adds those label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id)))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when remove_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:remove_labels) { [bug, regression, merge_requests] }
+
+ it 'removes those label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when add_label_ids and remove_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:add_labels) { [bug] }
+ let(:remove_labels) { [merge_requests] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'removes the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when add_label_ids and label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] }
+ let(:labels) { [merge_requests] }
+ let(:add_labels) { [regression] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id))
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_no_labels.label_ids).to be_empty
+ end
+ end
+
+ context 'when remove_label_ids and label_ids are passed' do
+ let(:issues) { [issue_no_labels, issue_bug_and_regression] }
+ let(:labels) { [merge_requests] }
+ let(:remove_labels) { [regression] }
+
+ it 'remove the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id)
+ end
+ end
+
+ context 'when add_label_ids, remove_label_ids, and label_ids are passed' do
+ let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] }
+ let(:labels) { [regression] }
+ let(:add_labels) { [bug] }
+ let(:remove_labels) { [merge_requests] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'removes the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+ end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index ac28b6f71f9..1ee9f3aae4d 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -54,8 +54,8 @@ describe Issues::CreateService, services: true do
label_ids: [label.id] }
end
- it 'does not assign label'do
- expect(issue.labels).to_not include label
+ it 'does not assign label' do
+ expect(issue.labels).not_to include label
end
end
@@ -69,7 +69,7 @@ describe Issues::CreateService, services: true do
end
it 'does not assign milestone' do
- expect(issue.milestone).to_not eq milestone
+ expect(issue.milestone).not_to eq milestone
end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index c15e26189a5..93bf0f64963 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do
let!(:milestone2) do
create(:milestone, project_id: new_project.id, title: 'v9.0')
end
+ let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
let!(:new_issue) { move_service.execute(old_issue, new_project) }
end
@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do
it 'preserves create time' do
expect(old_issue.created_at).to eq new_issue.created_at
end
+
+ it 'moves the award emoji' do
+ expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
+ end
end
context 'issue with notes' do
@@ -194,10 +199,10 @@ describe Issues::MoveService, services: true do
include_context 'issue move executed'
it 'rewrites uploads in description' do
- expect(new_issue.description).to_not eq description
+ expect(new_issue.description).not_to eq description
expect(new_issue.description)
.to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
- expect(new_issue.description).to_not include uploader.secret
+ expect(new_issue.description).not_to include uploader.secret
end
end
end
@@ -231,7 +236,7 @@ describe Issues::MoveService, services: true do
context 'user is reporter in both projects' do
include_context 'user can move issue'
- it { expect { move }.to_not raise_error }
+ it { expect { move }.not_to raise_error }
end
context 'user is reporter only in new project' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 52f69306994..dacbcd8fb46 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe Issues::UpdateService, services: true do
@@ -27,11 +28,6 @@ describe Issues::UpdateService, services: true do
end
end
- def update_issue(opts)
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
- @issue.reload
- end
-
context "valid params" do
before do
opts = {
@@ -39,7 +35,8 @@ describe Issues::UpdateService, services: true do
description: 'Also please fix',
assignee_id: user2.id,
state_event: 'close',
- label_ids: [label.id]
+ label_ids: [label.id],
+ confidential: true
}
perform_enqueued_jobs do
@@ -79,13 +76,25 @@ describe Issues::UpdateService, services: true do
end
it 'creates system note about title change' do
- note = find_note('Title changed')
+ note = find_note('Changed title:')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ end
+
+ it 'creates system note about confidentiality change' do
+ note = find_note('Made the issue confidential')
expect(note).not_to be_nil
- expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+ expect(note.note).to eq 'Made the issue confidential'
end
end
+ def update_issue(opts)
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue.reload
+ end
+
context 'todos' do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -265,5 +274,50 @@ describe Issues::UpdateService, services: true do
end
end
end
+
+ context 'updating labels' do
+ let(:label3) { create(:label, project: project) }
+ let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
+
+ context 'when add_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
+
+ it 'ignores the label_ids parameter' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+
+ it 'adds the passed labels' do
+ expect(result.label_ids).to include(label3.id)
+ end
+ end
+
+ context 'when remove_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
+
+ before { issue.update_attributes(labels: [label, label3]) }
+
+ it 'ignores the label_ids parameter' do
+ expect(result.label_ids).not_to be_empty
+ end
+
+ it 'removes the passed labels' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+ end
+
+ context 'when add_label_ids and remove_label_ids are passed' do
+ let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } }
+
+ before { issue.update_attributes(labels: [label]) }
+
+ it 'adds the passed labels' do
+ expect(result.label_ids).to include(label3.id)
+ end
+
+ it 'removes the passed labels' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+ end
+ end
end
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
new file mode 100644
index 00000000000..2395445e7fd
--- /dev/null
+++ b/spec/services/members/destroy_service_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Members::DestroyService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:member) { create(:project_member, source: project) }
+
+ context 'when member is nil' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user cannot destroy the given member' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user can destroy the given member' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'destroys the member' do
+ destroy_member(member, user)
+
+ expect(member).to be_destroyed
+ end
+
+ context 'when the given member is a requester' do
+ before do
+ member.update_column(:requested_at, Time.now)
+ end
+
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
+
+ destroy_member(member, user)
+ end
+
+ context 'when current user is the member' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+
+ destroy_member(member, member.user)
+ end
+ end
+
+ context 'when current user is the member and ' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+
+ destroy_member(member, member.user)
+ end
+ end
+ end
+ end
+
+ def destroy_member(member, user)
+ Members::DestroyService.new(member, user).execute
+ end
+end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
new file mode 100644
index 00000000000..dd656c3bbb7
--- /dev/null
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+# Write specs in this file.
+describe MergeRequests::AddTodoWhenBuildFailsService do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { create(:project) }
+ let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
+ let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
+ let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
+ let(:todo_service) { TodoService.new }
+
+ let(:merge_request) do
+ create(:merge_request, merge_user: user, source_branch: 'master',
+ target_branch: 'feature', source_project: project, target_project: project,
+ state: 'opened')
+ end
+
+ before do
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(service).to receive(:todo_service).and_return(todo_service)
+ end
+
+ describe '#execute' do
+ context 'commit status with ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
+ service.execute(commit_status)
+ end
+ end
+
+ context 'commit status with non-HEAD ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+ service.execute(commit_status)
+ end
+ end
+
+ context 'commit status without ref' do
+ let(:commit_status) { create(:generic_commit_status) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+ service.execute(commit_status)
+ end
+ end
+ end
+
+ describe '#close' do
+ context 'commit status with ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
+ service.close(commit_status)
+ end
+ end
+
+ context 'commit status with non-HEAD ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_retried)
+ service.close(commit_status)
+ end
+ end
+
+ context 'commit status without ref' do
+ let(:commit_status) { create(:generic_commit_status) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_retried)
+ service.close(commit_status)
+ 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 120f4d6a669..e433f49872d 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -12,7 +12,8 @@ describe MergeRequests::CreateService, services: true do
title: 'Awesome merge_request',
description: 'please fix',
source_branch: 'feature',
- target_branch: 'master'
+ target_branch: 'master',
+ force_remove_source_branch: '1'
}
end
@@ -29,6 +30,7 @@ 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 { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'should execute hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ceb3f97280e..2f72cd60071 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,21 @@ describe MergeRequests::MergeService, services: true do
end
end
+ context 'remove source branch by author' do
+ let(:service) do
+ merge_request.merge_params['force_remove_source_branch'] = '1'
+ merge_request.save!
+ MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message')
+ end
+
+ it 'removes the source branch' do
+ expect(DeleteBranchService).to receive(:new).
+ with(merge_request.source_project, merge_request.author).
+ and_call_original
+ service.execute(merge_request)
+ end
+ end
+
context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
@@ -50,6 +65,16 @@ describe MergeRequests::MergeService, services: true do
expect(merge_request.merge_error).to eq("Something went wrong during merge")
end
+
+ it 'saves error if there is an PreReceiveError exception' do
+ allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error")
+
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error).to eq("error")
+ 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 52a302e0e1a..4da8146e3d6 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
@@ -1,8 +1,8 @@
require 'spec_helper'
describe MergeRequests::MergeWhenBuildSucceedsService do
- let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
@@ -10,14 +10,18 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
source_project: project, target_project: project, state: "opened")
end
- let(:project) { create(:project) }
- let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+ let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) }
let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') }
describe "#execute" do
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project,
+ source_branch: "feature", target_branch: 'master')
+ end
+
context 'first time enabling' do
before do
- allow(merge_request).to receive(:ci_commit).and_return(ci_commit)
+ allow(merge_request).to receive(:pipeline).and_return(pipeline)
service.execute(merge_request)
end
@@ -39,9 +43,9 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
- allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit)
+ allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline)
allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true)
- allow(ci_commit).to receive(:success?).and_return(true)
+ allow(pipeline).to receive(:success?).and_return(true)
end
it 'updates the merge params' do
@@ -58,8 +62,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService 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)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async)
service.trigger(build)
@@ -71,11 +75,11 @@ describe MergeRequests::MergeWhenBuildSucceedsService 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)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
allow(old_build).to receive(:sha).and_return('1234abcdef')
- expect(MergeWorker).to_not receive(:perform_async)
+ expect(MergeWorker).not_to receive(:perform_async)
service.trigger(old_build)
end
end
@@ -88,16 +92,16 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
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)
+ expect(MergeWorker).not_to 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)
+ allow(pipeline).to receive(:success?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async)
service.trigger(commit_status)
@@ -106,23 +110,23 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
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') }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
+ let(:test) { create(:ci_build, pipeline: pipeline, 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)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do
+ Ci::Pipeline.find(pipeline.id)
end
# We create test after the build
- allow(ci_commit).to receive(:create_next_builds).and_wrap_original do
+ allow(pipeline).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)
+ expect(MergeWorker).not_to receive(:perform_async)
build.success
test.drop
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fea8182bd30..7d5cb876063 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do
target_branch: 'feature',
target_project: @project)
+ @build_failed_todo = create(:todo,
+ :build_failed,
+ user: @user,
+ project: @project,
+ target: @merge_request,
+ author: @user)
+
+ @fork_build_failed_todo = create(:todo,
+ :build_failed,
+ user: @user,
+ project: @project,
+ target: @merge_request,
+ author: @user)
+
@commits = @merge_request.commits
@oldrev = @commits.last.id
@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push to origin repo target branch' do
@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'manual merge of source branch' do
@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do
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') }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo source branch' do
@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo target branch' do
@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to origin repo target branch after fork project was removed' do
@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push new branch that exists in a merge request' do
@@ -149,10 +175,11 @@ describe MergeRequests::RefreshService, services: true do
end
end
-
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
+ @build_failed_todo.reload
+ @fork_build_failed_todo.reload
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 213e8c2eb3a..d4ebe28c276 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -39,7 +39,8 @@ describe MergeRequests::UpdateService, services: true do
assignee_id: user2.id,
state_event: 'close',
label_ids: [label.id],
- target_branch: 'target'
+ target_branch: 'target',
+ force_remove_source_branch: '1'
}
end
@@ -61,6 +62,7 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.labels.count).to eq(1) }
it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') }
+ it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'should execute hooks with update action' do
expect(service).to have_received(:execute_hooks).
@@ -90,10 +92,10 @@ describe MergeRequests::UpdateService, services: true do
end
it 'creates system note about title change' do
- note = find_note('Title changed')
+ note = find_note('Changed title:')
expect(note).not_to be_nil
- expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
end
it 'creates system note about branch change' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index ff23f13e1cb..35f576874b8 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
-
+
@note = Notes::CreateService.new(project, user, opts).execute
end
@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
project.team << [user, :master]
end
- it "creates emoji note" do
+ it "creates an award emoji" do
opts = {
note: ':smile: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
+ note = Notes::CreateService.new(project, user, opts).execute
- @note = Notes::CreateService.new(project, user, opts).execute
-
- expect(@note).to be_valid
- expect(@note.note).to eq('smile')
- expect(@note.is_award).to be_truthy
+ expect(note).to be_valid
+ expect(note.name).to eq('smile')
end
it "creates regular note if emoji name is invalid" do
@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_valid
+ expect(note.note).to eq(opts[:note])
+ end
+
+ it "normalizes the emoji name" do
+ opts = {
+ note: ':+1:',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
- @note = Notes::CreateService.new(project, user, opts).execute
+ expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
- expect(@note).to be_valid
- expect(@note.note).to eq(opts[:note])
- expect(@note.is_award).to be_falsy
+ Notes::CreateService.new(project, user, opts).execute
end
end
end
diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb
new file mode 100644
index 00000000000..110efb54fa0
--- /dev/null
+++ b/spec/services/notes/diff_position_update_service_spec.rb
@@ -0,0 +1,175 @@
+require 'spec_helper'
+
+describe Notes::DiffPositionUpdateService, services: true do
+ let(:project) { create(:project) }
+ let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
+ let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
+ let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
+
+ let(:path) { "files/ruby/popen.rb" }
+
+ let(:old_diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: create_commit.parent_id,
+ head_sha: modify_commit.sha
+ )
+ end
+
+ let(:new_diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: create_commit.parent_id,
+ head_sha: edit_commit.sha
+ )
+ end
+
+ subject do
+ described_class.new(
+ project,
+ nil,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ paths: [path]
+ )
+ end
+
+ # old diff:
+ # 1 + require 'fileutils'
+ # 2 + require 'open3'
+ # 3 +
+ # 4 + module Popen
+ # 5 + extend self
+ # 6 +
+ # 7 + def popen(cmd, path=nil)
+ # 8 + unless cmd.is_a?(Array)
+ # 9 + raise "System commands must be given as an array of strings"
+ # 10 + end
+ # 11 +
+ # 12 + path ||= Dir.pwd
+ # 13 + vars = { "PWD" => path }
+ # 14 + options = { chdir: path }
+ # 15 +
+ # 16 + unless File.directory?(path)
+ # 17 + FileUtils.mkdir_p(path)
+ # 18 + end
+ # 19 +
+ # 20 + @cmd_output = ""
+ # 21 + @cmd_status = 0
+ # 22 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ # 23 + @cmd_output << stdout.read
+ # 24 + @cmd_output << stderr.read
+ # 25 + @cmd_status = wait_thr.value.exitstatus
+ # 26 + end
+ # 27 +
+ # 28 + return @cmd_output, @cmd_status
+ # 29 + end
+ # 30 + end
+ #
+ # new diff:
+ # 1 + require 'fileutils'
+ # 2 + require 'open3'
+ # 3 +
+ # 4 + module Popen
+ # 5 + extend self
+ # 6 +
+ # 7 + def popen(cmd, path=nil)
+ # 8 + unless cmd.is_a?(Array)
+ # 9 + raise RuntimeError, "System commands must be given as an array of strings"
+ # 10 + end
+ # 11 +
+ # 12 + path ||= Dir.pwd
+ # 13 +
+ # 14 + vars = {
+ # 15 + "PWD" => path
+ # 16 + }
+ # 17 +
+ # 18 + options = {
+ # 19 + chdir: path
+ # 20 + }
+ # 21 +
+ # 22 + unless File.directory?(path)
+ # 23 + FileUtils.mkdir_p(path)
+ # 24 + end
+ # 25 +
+ # 26 + @cmd_output = ""
+ # 27 + @cmd_status = 0
+ # 28 +
+ # 29 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ # 30 + @cmd_output << stdout.read
+ # 31 + @cmd_output << stderr.read
+ # 32 + @cmd_status = wait_thr.value.exitstatus
+ # 33 + end
+ # 34 +
+ # 35 + return @cmd_output, @cmd_status
+ # 36 + end
+ # 37 + end
+ #
+ # old->new diff:
+ # .. .. @@ -6,12 +6,18 @@ module Popen
+ # 6 6
+ # 7 7 def popen(cmd, path=nil)
+ # 8 8 unless cmd.is_a?(Array)
+ # 9 - raise "System commands must be given as an array of strings"
+ # 9 + raise RuntimeError, "System commands must be given as an array of strings"
+ # 10 10 end
+ # 11 11
+ # 12 12 path ||= Dir.pwd
+ # 13 - vars = { "PWD" => path }
+ # 14 - options = { chdir: path }
+ # 13 +
+ # 14 + vars = {
+ # 15 + "PWD" => path
+ # 16 + }
+ # 17 +
+ # 18 + options = {
+ # 19 + chdir: path
+ # 20 + }
+ # 15 21
+ # 16 22 unless File.directory?(path)
+ # 17 23 FileUtils.mkdir_p(path)
+ # 18 24 end
+ # 19 25
+ # 20 26 @cmd_output = ""
+ # 21 27 @cmd_status = 0
+ # 28 +
+ # 22 29 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ # 23 30 @cmd_output << stdout.read
+ # 24 31 @cmd_output << stderr.read
+ # .. ..
+
+ describe "#execute" do
+ let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) }
+
+ let(:old_position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: line,
+ diff_refs: old_diff_refs
+ )
+ end
+
+ context "when the diff line is the same" do
+ let(:line) { 16 }
+
+ it "updates the position" do
+ subject.execute(note)
+
+ expect(note.original_position).to eq(old_position)
+ expect(note.position).not_to eq(old_position)
+ expect(note.position.new_line).to eq(22)
+ end
+ end
+
+ context "when the diff line has changed" do
+ let(:line) { 9 }
+
+ it "doesn't update the position" do
+ subject.execute(note)
+
+ expect(note.original_position).to eq(old_position)
+ expect(note.position).to eq(old_position)
+ end
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 4bbc4ddc3ab..54719cbb8d8 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -46,6 +46,8 @@ describe NotificationService, services: true do
project.team << [issue.assignee, :master]
project.team << [note.author, :master]
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
+ update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_custom_global)
end
describe :new_note do
@@ -53,7 +55,7 @@ describe NotificationService, services: true 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
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
ActionMailer::Base.deliveries.clear
@@ -62,15 +64,19 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignee)
+ should_email(@u_custom_global)
should_email(@u_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_guest_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@unsubscriber)
should_not_email(@u_outsider_mentioned)
+ should_not_email(@u_lazy_participant)
end
it 'filters out "mentioned in" notes' do
@@ -79,6 +85,19 @@ describe NotificationService, services: true do
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
end
+
+ context 'participating' do
+ context 'by note' do
+ before do
+ ActionMailer::Base.deliveries.clear
+ note.author = @u_lazy_participant
+ note.save
+ notification.new_note(note)
+ end
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe 'new note on issue in project that belongs to a group' do
@@ -87,10 +106,12 @@ describe NotificationService, services: true do
before do
note.project.namespace_id = group.id
note.project.group.add_user(@u_watcher, GroupMember::MASTER)
+ note.project.group.add_user(@u_custom_global, GroupMember::MASTER)
note.project.save
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global!
+ update_custom_notification(:new_note, @u_custom_global)
ActionMailer::Base.deliveries.clear
end
@@ -100,10 +121,14 @@ describe NotificationService, services: true do
should_email(note.noteable.author)
should_email(note.noteable.assignee)
should_email(@u_mentioned)
+ should_email(@u_custom_global)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_guest_watcher)
should_not_email(@u_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
end
end
@@ -114,12 +139,15 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
+ let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
it 'filters out users that can not read the issue' do
project.team << [member, :developer]
+ project.team << [guest, :guest]
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
@@ -128,6 +156,8 @@ describe NotificationService, services: true do
notification.new_note(note)
should_not_email(non_member)
+ should_not_email(guest)
+ should_not_email(guest_watcher)
should_email(author)
should_email(assignee)
should_email(member)
@@ -160,6 +190,7 @@ describe NotificationService, services: true do
should_email(member)
end
+ should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignee)
should_not_email(note.author)
@@ -201,6 +232,10 @@ describe NotificationService, services: true do
should_email(member)
end
+ # it emails custom global users on mention
+ should_email(@u_custom_global)
+
+ should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_not_email(note.author)
should_email(@u_mentioned)
@@ -218,34 +253,41 @@ describe NotificationService, services: true do
build_team(note.project)
ActionMailer::Base.deliveries.clear
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
+ update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_custom_global)
end
describe '#new_note, #perform_enqueued_jobs' do
it do
notification.new_note(note)
-
+ should_email(@u_guest_watcher)
+ should_email(@u_custom_global)
+ should_email(@u_guest_custom)
should_email(@u_committer)
should_email(@u_watcher)
should_not_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
note.update_attribute(:note, '@mention referenced')
notification.new_note(note)
+ should_email(@u_guest_watcher)
should_email(@u_committer)
should_email(@u_watcher)
should_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- @u_committer.update_attributes(notification_level: :mention)
+ @u_committer = create_global_setting_for(@u_committer, :mention)
notification.new_note(note)
should_not_email(@u_committer)
end
@@ -261,6 +303,8 @@ describe NotificationService, services: true do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
ActionMailer::Base.deliveries.clear
+ update_custom_notification(:new_issue, @u_guest_custom, project)
+ update_custom_notification(:new_issue, @u_custom_global)
end
describe '#new_issue' do
@@ -269,14 +313,18 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- issue.assignee.update_attributes(notification_level: :mention)
+ create_global_setting_for(issue.assignee, :mention)
notification.new_issue(issue, @u_disabled)
should_not_email(issue.assignee)
@@ -296,25 +344,30 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
label.toggle_subscription(member)
+ label.toggle_subscription(guest)
label.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
notification.new_issue(confidential_issue, @u_disabled)
+ should_not_email(@u_guest_watcher)
should_not_email(non_member)
should_not_email(author)
+ should_not_email(guest)
should_email(assignee)
should_email(member)
should_email(admin)
@@ -323,16 +376,25 @@ describe NotificationService, services: true do
end
describe '#reassigned_issue' do
+ before do
+ update_custom_notification(:reassign_issue, @u_guest_custom, project)
+ update_custom_notification(:reassign_issue, @u_custom_global)
+ end
+
it 'emails new assignee' do
notification.reassigned_issue(issue, @u_disabled)
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails previous assignee even if he has the "on mention" notif level' do
@@ -342,11 +404,15 @@ describe NotificationService, services: true do
should_email(@u_mentioned)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee even if he has the "on mention" notif level' do
@@ -356,11 +422,15 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee' do
@@ -370,11 +440,15 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'does not email new assignee if they are the current user' do
@@ -383,12 +457,44 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(issue.assignee)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -411,6 +517,7 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
should_not_email(issue.author)
should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
should_not_email(@u_participant_mentioned)
should_not_email(@subscriber)
should_not_email(@watcher_and_subscriber)
@@ -425,6 +532,7 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
let!(:label_1) { create(:label, issues: [confidential_issue]) }
@@ -432,11 +540,13 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label_2.toggle_subscription(non_member)
label_2.toggle_subscription(author)
label_2.toggle_subscription(assignee)
label_2.toggle_subscription(member)
+ label_2.toggle_subscription(guest)
label_2.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -444,6 +554,7 @@ describe NotificationService, services: true do
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -453,33 +564,107 @@ describe NotificationService, services: true do
end
describe '#close_issue' do
+ before do
+ update_custom_notification(:close_issue, @u_guest_custom, project)
+ update_custom_notification(:close_issue, @u_custom_global)
+ end
+
it 'should sent email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
should_email(issue.assignee)
should_email(issue.author)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
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)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
describe '#reopen_issue' do
+ before do
+ update_custom_notification(:reopen_issue, @u_guest_custom, project)
+ update_custom_notification(:reopen_issue, @u_custom_global)
+ end
+
it 'should send email to issue assignee and issue author' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
should_email(issue.author)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
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_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -495,6 +680,11 @@ describe NotificationService, services: true do
end
describe '#new_merge_request' do
+ before do
+ update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_custom_global)
+ end
+
it do
notification.new_merge_request(merge_request, @u_disabled)
@@ -502,8 +692,12 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it "emails subscribers of the merge request's labels" do
@@ -514,9 +708,43 @@ describe NotificationService, services: true do
should_email(subscriber)
end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.new_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe '#reassigned_merge_request' do
+ before do
+ update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reassign_merge_request, @u_custom_global)
+ end
+
it do
notification.reassigned_merge_request(merge_request, merge_request.author)
@@ -525,9 +753,42 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -555,27 +816,71 @@ describe NotificationService, services: true do
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
end
describe '#closed_merge_request' do
+ before do
+ update_custom_notification(:close_merge_request, @u_guest_custom, project)
+ update_custom_notification(:close_merge_request, @u_custom_global)
+ end
+
it do
notification.close_mr(merge_request, @u_disabled)
should_email(merge_request.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
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)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
describe '#merged_merge_request' do
+ before do
+ update_custom_notification(:merge_merge_request, @u_guest_custom, project)
+ update_custom_notification(:merge_merge_request, @u_custom_global)
+ end
+
it do
notification.merge_mr(merge_request, @u_disabled)
@@ -584,13 +889,51 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_email(@u_guest_watcher)
+ should_email(@u_custom_global)
+ should_email(@u_guest_custom)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.merge_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
describe '#reopen_merge_request' do
+ before do
+ update_custom_notification(:reopen_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reopen_merge_request, @u_custom_global)
+ end
+
it do
notification.reopen_mr(merge_request, @u_disabled)
@@ -599,9 +942,42 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -620,20 +996,33 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participating)
+ should_email(@u_lazy_participant)
+ should_email(@u_custom_global)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@u_guest_custom)
should_not_email(@u_disabled)
end
end
end
def build_team(project)
- @u_watcher = create(:user, notification_level: :watch)
- @u_participating = create(:user, notification_level: :participating)
- @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating)
- @u_disabled = create(:user, notification_level: :disabled)
- @u_mentioned = create(:user, username: 'mention', notification_level: :mention)
- @u_committer = create(:user, username: 'committer')
- @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating)
- @u_outsider_mentioned = create(:user, username: 'outsider')
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participating = create_global_setting_for(create(:user), :participating)
+ @u_participant_mentioned = create_global_setting_for(create(:user, username: 'participant'), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mention'), :mention)
+ @u_committer = create(:user, username: 'committer')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+ @u_outsider_mentioned = create(:user, username: 'outsider')
+ @u_custom_global = create_global_setting_for(create(:user, username: 'custom_global'), :custom)
+
+ # User to be participant by default
+ # This user does not contain any record in notification settings table
+ # It should be treated with a :participating notification_level
+ @u_lazy_participant = create(:user, username: 'lazy-participant')
+
+ @u_guest_watcher = create_user_with_notification(:watch, 'guest_watching')
+ @u_guest_custom = create_user_with_notification(:custom, 'guest_custom')
project.team << [@u_watcher, :master]
project.team << [@u_participating, :master]
@@ -642,13 +1031,40 @@ describe NotificationService, services: true do
project.team << [@u_mentioned, :master]
project.team << [@u_committer, :master]
project.team << [@u_not_mentioned, :master]
+ project.team << [@u_lazy_participant, :master]
+ project.team << [@u_custom_global, :master]
+ end
+
+ def create_global_setting_for(user, level)
+ setting = user.global_notification_setting
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ def create_user_with_notification(level, username)
+ user = create(:user, username: username)
+ setting = user.notification_settings_for(project)
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ # Create custom notifications
+ # When resource is nil it means global notification
+ def update_custom_notification(event, user, resource = nil)
+ setting = user.notification_settings_for(resource)
+ setting.events[event] = true
+ setting.save
end
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
- @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating)
- @watcher_and_subscriber = create(:user, notification_level: :watch)
+ @subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
+ @watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 6108c26a78b..0971fec2e9f 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -33,6 +33,18 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ 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)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e43903dbd3c..fd114359467 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -64,7 +64,7 @@ describe Projects::CreateService, services: true do
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end
- it { expect(File.exists?(@path)).to be_truthy }
+ it { expect(File.exist?(@path)).to be_truthy }
end
context 'wiki_enabled false does not create wiki repository directory' do
@@ -74,7 +74,7 @@ describe Projects::CreateService, services: true do
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end
- it { expect(File.exists?(@path)).to be_falsey }
+ it { expect(File.exist?(@path)).to be_falsey }
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 1ec27077717..29341c5e57e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -13,8 +13,8 @@ describe Projects::DestroyService, services: true do
end
it { expect(Project.all).not_to include(project) }
- it { expect(Dir.exists?(path)).to be_falsey }
- it { expect(Dir.exists?(remove_path)).to be_falsey }
+ it { expect(Dir.exist?(path)).to be_falsey }
+ it { expect(Dir.exist?(remove_path)).to be_falsey }
end
context 'Sidekiq fake' do
@@ -24,8 +24,31 @@ describe Projects::DestroyService, services: true do
end
it { expect(Project.all).not_to include(project) }
- it { expect(Dir.exists?(path)).to be_falsey }
- it { expect(Dir.exists?(remove_path)).to be_truthy }
+ it { expect(Dir.exist?(path)).to be_falsey }
+ it { expect(Dir.exist?(remove_path)).to be_truthy }
+ end
+
+ context 'container registry' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ context 'tags deletion succeeds' do
+ it do
+ expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
+
+ destroy_project(project, user, {})
+ end
+ end
+
+ context 'tags deletion fails' do
+ before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) }
+
+ subject { destroy_project(project, user, {}) }
+
+ it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) }
+ end
end
def destroy_project(project, user, params)
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index d1ee60a0aea..31bb7120d84 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -42,6 +42,33 @@ describe Projects::ForkService, services: true do
expect(@to_project.builds_enabled?).to be_truthy
end
end
+
+ context "when project has restricted visibility level" do
+ context "and only one visibility level is restricted" do
+ before do
+ @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ it "creates fork with highest allowed level" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context "and all visibility levels are restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ end
+
+ it "creates fork with private visibility levels" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
end
describe :fork_to_namespace do
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 4c5ced7e746..bd4dc6a0f79 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -12,7 +12,7 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
- expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.path_with_namespace)
+ expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.repository_storage_path, project.path_with_namespace)
subject.execute
expect(project.pushes_since_gc).to eq(0)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 7f2dcdab960..d5d4d7c56ef 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -36,7 +36,7 @@ describe Projects::ImportService, services: true do
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)
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_return(true)
result = subject.execute
@@ -44,12 +44,12 @@ describe Projects::ImportService, services: true do
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'))
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, 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'
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
end
end
@@ -64,7 +64,7 @@ describe Projects::ImportService, services: true do
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::Shell).to receive(:import_repository).with(project.repository_storage_path, 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
@@ -74,7 +74,7 @@ describe Projects::ImportService, services: true do
it 'flushes various caches' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).
- with(project.path_with_namespace, project.import_url).
+ with(project.repository_storage_path, project.path_with_namespace, project.import_url).
and_return(true)
expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).
@@ -90,7 +90,7 @@ describe Projects::ImportService, services: true do
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::Shell).to receive(:import_repository).with(project.repository_storage_path, 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
@@ -100,7 +100,7 @@ describe Projects::ImportService, services: true do
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::Shell).to receive(:import_repository).with(project.repository_storage_path, 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
@@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do
}
)
- Gitlab.config.omniauth.providers << provider
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 06017317339..57c71544dff 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(user.namespace) }
end
+ context 'disallow transfering of project with tags' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ subject { transfer_project(project, user, group) }
+
+ it { is_expected.to be_falsey }
+ end
+
context 'namespace -> not allowed namespace' do
before do
@result = transfer_project(project, user, group)
@@ -60,5 +71,4 @@ describe Projects::TransferService, services: true do
it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end
end
-
end
diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb
new file mode 100644
index 00000000000..14f3301d9f4
--- /dev/null
+++ b/spec/services/search/snippet_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Search::SnippetService, services: true do
+ let(:author) { create(:author) }
+ let(:project) { create(:empty_project) }
+
+ let!(:public_snippet) { create(:snippet, :public, content: 'password: XXX') }
+ let!(:internal_snippet) { create(:snippet, :internal, content: 'password: XXX') }
+ let!(:private_snippet) { create(:snippet, :private, content: 'password: XXX', author: author) }
+
+ let!(:project_public_snippet) { create(:snippet, :public, project: project, content: 'password: XXX') }
+ let!(:project_internal_snippet) { create(:snippet, :internal, project: project, content: 'password: XXX') }
+ let!(:project_private_snippet) { create(:snippet, :private, project: project, content: 'password: XXX') }
+
+ describe '#execute' do
+ context 'unauthenticated' do
+ it 'returns public snippets only' do
+ search = described_class.new(nil, search: 'password')
+ results = search.execute
+
+ expect(results.objects('snippet_blobs')).to match_array [public_snippet, project_public_snippet]
+ end
+ end
+
+ context 'authenticated' do
+ it 'returns only public & internal snippets for regular users' do
+ user = create(:user)
+ search = described_class.new(user, search: 'password')
+ results = search.execute
+
+ expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
+ end
+
+ it 'returns public, internal snippets and project private snippets for project members' do
+ member = create(:user)
+ project.team << [member, :developer]
+ search = described_class.new(member, search: 'password')
+ results = search.execute
+
+ expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ end
+
+ it 'returns public, internal and private snippets where user is the author' do
+ search = described_class.new(author, search: 'password')
+ results = search.execute
+
+ expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
+ end
+
+ it 'returns all snippets when user is admin' do
+ admin = create(:admin)
+ search = described_class.new(admin, search: 'password')
+ results = search.execute
+
+ expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5fbf2ae5247..43693441450 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -208,10 +208,12 @@ describe SystemNoteService, services: true do
end
describe '.merge_when_build_succeeds' do
- let(:ci_commit) { build :ci_commit_without_jobs }
- let(:noteable) { create :merge_request }
+ let(:pipeline) { build(:ci_pipeline_without_jobs )}
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
- subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) }
+ subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) }
it_behaves_like 'a system note'
@@ -221,8 +223,9 @@ describe SystemNoteService, services: true do
end
describe '.cancel_merge_when_build_succeeds' do
- let(:ci_commit) { build :ci_commit_without_jobs }
- let(:noteable) { create :merge_request }
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
@@ -241,15 +244,19 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
expect(subject.note).
- to eq "Title changed from **Old title** to **#{noteable.title}**"
+ to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**"
end
end
+ end
- context 'when noteable does not respond to `title' do
- let(:noteable) { double('noteable') }
+ describe '.change_issue_confidentiality' do
+ subject { described_class.change_issue_confidentiality(noteable, project, author) }
- it 'returns nil' do
- expect(subject).to be_nil
+ context 'when noteable responds to `confidential`' do
+ it_behaves_like 'a system note'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'Made the issue visible'
end
end
end
@@ -522,7 +529,7 @@ describe SystemNoteService, services: true do
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:jira_issue) { JiraIssue.new("JIRA-1", project)}
+ let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
let(:commit) { project.commit }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a075496ee63..b4522536724 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -5,20 +5,22 @@ describe TodoService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
+ project.team << [guest, :guest]
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
end
describe 'Issues' do
- let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) }
+ let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
@@ -41,18 +43,20 @@ describe TodoService, services: true do
service.new_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: issue, action: Todo::MENTIONED)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
@@ -81,6 +85,7 @@ describe TodoService, services: true do
service.update_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: issue, action: Todo::MENTIONED)
@@ -92,15 +97,37 @@ describe TodoService, services: true do
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+
+ context 'issues with a task list' do
+ it 'does not create todo when tasks are marked as completed' do
+ issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+
+ service.update_issue(issue, author)
+
+ should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, 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: member, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ end
+
+ it 'does not raise an error when description not change' do
+ issue.update(title: 'Sample')
+
+ expect { service.update_issue(issue, author) }.not_to raise_error
+ end
+ end
end
describe '#close_issue' do
@@ -146,6 +173,48 @@ describe TodoService, services: true do
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
+
+ describe 'cached counts' do
+ it 'updates when todos change' do
+ create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
+ end
+ end
+
+ describe '#mark_todos_as_done' do
+ it 'marks related todos 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_todos_as_done([first_todo, second_todo], john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+
+ describe 'cached counts' do
+ it 'updates when todos change' do
+ todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.mark_todos_as_done([todo], john_doe)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
+ end
end
describe '#new_note' do
@@ -156,7 +225,6 @@ describe TodoService, services: true do
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
- let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
it 'mark related pending todos to the noteable for the note author as done' do
@@ -169,13 +237,6 @@ describe TodoService, services: true do
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)
@@ -187,18 +248,20 @@ describe TodoService, services: true do
service.new_note(note, john_doe)
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, 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: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
@@ -215,10 +278,18 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a issue' do
+ service.mark_todo(unassigned_issue, author)
+
+ should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
+ 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_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
describe '#new_merge_request' do
@@ -240,6 +311,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -251,6 +323,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -261,6 +334,27 @@ describe TodoService, services: true do
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
+
+ context 'with a task list' do
+ it 'does not create todo when tasks are marked as completed' do
+ mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+
+ service.update_merge_request(mr_assigned, author)
+
+ should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, 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: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'does not raise an error when description not change' do
+ mr_assigned.update(title: 'Sample')
+
+ expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error
+ end
+ end
end
describe '#close_merge_request' do
@@ -305,6 +399,54 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
end
end
+
+ describe '#new_award_emoji' do
+ it 'marks related pending todos to the target for the user as done' do
+ todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author)
+ service.new_award_emoji(mr_assigned, john_doe)
+
+ expect(todo.reload).to be_done
+ end
+ end
+
+ describe '#merge_request_build_failed' do
+ it 'creates a pending todo for the merge request author' do
+ service.merge_request_build_failed(mr_unassigned)
+
+ should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
+ end
+
+ describe '#merge_request_push' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe)
+ second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe)
+ service.merge_request_push(mr_assigned, author)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).not_to be_done
+ end
+ end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a merge request' do
+ service.mark_todo(mr_unassigned, author)
+
+ should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
+ end
+ end
+ end
+
+ it 'updates cached counts when a todo is created' do
+ issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
+
+ expect(john_doe.todos_pending_count).to eq(0)
+ expect(john_doe).to receive(:update_todos_count_cache)
+
+ service.new_issue(issue, author)
+
+ expect(Todo.where(user_id: john_doe.id, state: :pending).count).to eq 1
+ expect(john_doe.todos_pending_count).to eq(1)
end
def should_create_todo(attributes = {})
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 576d16e7ea3..606da1b7605 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,11 +3,6 @@ if ENV['SIMPLECOV']
SimpleCov.start :rails
end
-if ENV['COVERALLS']
- require 'coveralls'
- Coveralls.wear_merged!
-end
-
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
@@ -16,6 +11,11 @@ require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'rspec/retry'
+if ENV['CI']
+ require 'knapsack'
+ Knapsack::Adapters::RSpecAdapter.bind
+end
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
new file mode 100644
index 00000000000..553fe9f1fbc
--- /dev/null
+++ b/spec/support/fake_u2f_device.rb
@@ -0,0 +1,36 @@
+class FakeU2fDevice
+ def initialize(page)
+ @page = page
+ end
+
+ def respond_to_u2f_registration
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenges = @page.evaluate_script('gon.u2f.challenges')
+
+ json_response = u2f_device(app_id).register_response(challenges[0])
+
+ @page.execute_script("
+ u2f.register = function(appId, registerRequests, signRequests, callback) {
+ callback(#{json_response});
+ };
+ ")
+ end
+
+ def respond_to_u2f_authentication
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenges = @page.evaluate_script('gon.u2f.challenges')
+ json_response = u2f_device(app_id).sign_response(challenges[0])
+
+ @page.execute_script("
+ u2f.sign = function(appId, challenges, signRequests, callback) {
+ callback(#{json_response});
+ };
+ ")
+ end
+
+ private
+
+ def u2f_device(app_id)
+ @u2f_device ||= U2F::FakeU2F.new(app_id)
+ end
+end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index e849a9633b9..a8e454eb09e 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -40,8 +40,7 @@ module FilterSpecHelper
filters = [
Banzai::Filter::AutolinkFilter,
- described_class,
- Banzai::Filter::ReferenceGathererFilter
+ described_class
]
HTML::Pipeline.new(filters, context)
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
new file mode 100644
index 00000000000..3ceec506401
--- /dev/null
+++ b/spec/support/import_export/import_export.yml
@@ -0,0 +1,20 @@
+# Class relationships to be included in the project import/export
+project_tree:
+ - :issues
+ - :labels
+ - merge_requests:
+ - :merge_request_diff
+ - :merge_request_test
+ - commit_statuses:
+ - :commit
+
+included_attributes:
+ project:
+ - :name
+ - :path
+ merge_requests:
+ - :id
+
+excluded_attributes:
+ merge_requests:
+ - :iid \ No newline at end of file
diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/support/import_spec_helper.rb
index 9d7648e25a7..6710962f082 100644
--- a/spec/controllers/import/import_spec_helper.rb
+++ b/spec/support/import_spec_helper.rb
@@ -28,6 +28,6 @@ module ImportSpecHelper
app_id: 'asd123',
app_secret: 'asd123'
)
- Gitlab.config.omniauth.providers << provider
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index a3f496359b1..f3ea206f387 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,12 +1,11 @@
module JiraServiceHelper
-
def jira_service_settings
properties = {
- "title"=>"JIRA tracker",
- "project_url"=>"http://jira.example/issues/?jql=project=A",
- "issues_url"=>"http://jira.example/browse/JIRA-1",
- "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa",
- "api_url"=>"http://jira.example/rest/api/2"
+ "title" => "JIRA tracker",
+ "project_url" => "http://jira.example/issues/?jql=project=A",
+ "issues_url" => "http://jira.example/browse/JIRA-1",
+ "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa",
+ "api_url" => "http://jira.example/rest/api/2"
}
jira_tracker.update_attributes(properties: properties, active: true)
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index cd9fdc6f18e..ffdf2bb0a8a 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -26,18 +26,21 @@ module LoginHelpers
# Internal: Login as the specified user
#
- # user - User instance to login with
- def login_with(user)
+ # user - User instance to login with
+ # remember - Whether or not to check "Remember me" (default: false)
+ def login_with(user, remember: false)
visit new_user_session_path
fill_in "user_login", with: user.email
fill_in "user_password", with: "12345678"
+ check 'user_remember_me' if remember
click_button "Sign in"
Thread.current[:current_user] = user
end
# Requires Javascript driver.
def logout
- find(:css, ".fa.fa-sign-out").click
+ find(".header-user-dropdown-toggle").click
+ click_link "Sign out"
end
# Logout without JavaScript driver
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index b87cd6bbca2..a79386b5db9 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -32,6 +32,10 @@ class MarkdownFeature
@project_wiki ||= ProjectWiki.new(project, user)
end
+ def project_wiki_page
+ @project_wiki_page ||= build(:wiki_page, wiki: project_wiki)
+ end
+
def issue
@issue ||= create(:issue, project: project)
end
@@ -63,8 +67,12 @@ class MarkdownFeature
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
+ def simple_milestone
+ @simple_milestone ||= create(:milestone, name: 'gfm-milestone', project: project)
+ end
+
def milestone
- @milestone ||= create(:milestone, project: project)
+ @milestone ||= create(:milestone, name: 'next goal', project: project)
end
# Cross-references -----------------------------------------------------------
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 43cb6ef43f2..e005058ba5b 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -154,7 +154,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6)
end
end
@@ -168,6 +168,16 @@ module MarkdownMatchers
expect(actual).to have_selector('input[checked]', count: 3)
end
end
+
+ # InlineDiffFilter
+ matcher :parse_inline_diffs do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('span.idiff.addition', count: 2)
+ expect(actual).to have_selector('span.idiff.deletion', count: 2)
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/reference_parser_helpers.rb
new file mode 100644
index 00000000000..01689194eac
--- /dev/null
+++ b/spec/support/reference_parser_helpers.rb
@@ -0,0 +1,5 @@
+module ReferenceParserHelpers
+ def empty_html_link
+ Nokogiri::HTML.fragment('<a></a>').children[0]
+ end
+end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index eec2e681117..93f96cacc00 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -13,18 +13,35 @@ module StubGitlabCalls
allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
end
- def stub_ci_commit_to_return_yaml_file
- stub_ci_commit_yaml_file(gitlab_ci_yaml)
+ def stub_ci_pipeline_to_return_yaml_file
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
- def stub_ci_commit_yaml_file(ci_yaml)
- allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml }
+ def stub_ci_pipeline_yaml_file(ci_yaml)
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
end
def stub_ci_builds_disabled
allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
end
+ def stub_container_registry_config(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(registry_settings)
+ allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ end
+
+ def stub_container_registry_tags(*tags)
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return(
+ { "tags" => tags }
+ )
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
+ JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
+ )
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
+ File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
+ )
+ end
+
private
def gitlab_url
@@ -36,20 +53,20 @@ module StubGitlabCalls
stub_request(:post, "#{gitlab_url}api/v3/session.json").
with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}",
- headers: { 'Content-Type'=>'application/json' }).
- to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' })
+ headers: { 'Content-Type' => 'application/json' }).
+ to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' })
end
def stub_user
f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json'))
stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz").
- with(headers: { 'Content-Type'=>'application/json' }).
- to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ with(headers: { 'Content-Type' => 'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token").
- with(headers: { 'Content-Type'=>'application/json' }).
- to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ with(headers: { 'Content-Type' => 'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
end
def stub_project_8
@@ -66,19 +83,19 @@ module StubGitlabCalls
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
- with(headers: { 'Content-Type'=>'application/json' }).
- to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ with(headers: { 'Content-Type' => 'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
end
def stub_projects_owned
stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
- with(headers: { 'Content-Type'=>'application/json' }).
+ with(headers: { 'Content-Type' => 'application/json' }).
to_return(status: 200, body: "", headers: {})
end
def stub_ci_enable
stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz").
- with(headers: { 'Content-Type'=>'application/json' }).
+ with(headers: { 'Content-Type' => 'application/json' }).
to_return(status: 200, body: "", headers: {})
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 71664bb192e..6b99b0f24cb 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -16,6 +16,8 @@ module TestEnv
'master' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -61,7 +63,7 @@ module TestEnv
end
def disable_pre_receive
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
end
# Clean /tmp/tests
@@ -78,7 +80,7 @@ module TestEnv
end
def setup_gitlab_shell
- unless File.directory?(Rails.root.join(*%w(tmp tests gitlab-shell)))
+ unless File.directory?(Gitlab.config.gitlab_shell.path)
`rake gitlab:shell:install`
end
end
@@ -125,14 +127,14 @@ module TestEnv
def copy_repo(project)
base_repo_path = File.expand_path(factory_repo_path_bare)
- target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
end
def repos_path
- Gitlab.config.gitlab_shell.repos_path
+ Gitlab.config.repositories.storages.default
end
def backup_path
@@ -141,7 +143,7 @@ module TestEnv
def copy_forked_repo_with_submodules(project)
base_repo_path = File.expand_path(forked_repo_path_bare)
- target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 05fc4c4554f..d2c056d8e14 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require 'rake'
describe 'gitlab:app namespace rake task' do
+ let(:enable_registry) { true }
+
before :all do
Rake.application.rake_require 'tasks/gitlab/task_helpers'
Rake.application.rake_require 'tasks/gitlab/backup'
@@ -15,13 +17,17 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir_p('public/uploads')
end
+ before do
+ stub_container_registry_config(enabled: enable_registry)
+ end
+
def run_rake_task(task_name)
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs}.each do |subtask|
+ %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -65,11 +71,11 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end
-
end # backup_restore task
describe 'backup_create' do
@@ -91,55 +97,108 @@ describe 'gitlab:app namespace rake task' do
@backup_tar = tars_glob.first
end
- before do
- create_backup
- end
-
- after do
- FileUtils.rm(@backup_tar)
- end
+ context 'tar creation' do
+ before do
+ create_backup
+ end
- context 'archive file permissions' do
- it 'should set correct permissions on the tar file' do
- expect(File.exist?(@backup_tar)).to be_truthy
- expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
+ after do
+ FileUtils.rm(@backup_tar)
end
- context 'with custom archive_permissions' do
- before do
- allow(Gitlab.config.backup).to receive(:archive_permissions).and_return(0651)
- # We created a backup in a before(:all) so it got the default permissions.
- # We now need to do some work to create a _new_ backup file using our stub.
- FileUtils.rm(@backup_tar)
- create_backup
+ context 'archive file permissions' do
+ it 'should set correct permissions on the tar file' do
+ expect(File.exist?(@backup_tar)).to be_truthy
+ expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
end
- it 'uses the custom permissions' do
- expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100651')
+ context 'with custom archive_permissions' do
+ before do
+ allow(Gitlab.config.backup).to receive(:archive_permissions).and_return(0651)
+ # We created a backup in a before(:all) so it got the default permissions.
+ # We now need to do some work to create a _new_ backup file using our stub.
+ FileUtils.rm(@backup_tar)
+ create_backup
+ end
+
+ it 'uses the custom permissions' do
+ expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100651')
+ end
end
end
- end
- it 'should set correct permissions on the tar contents' do
- tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
- )
- expect(exit_status).to eq(0)
- expect(tar_contents).to match('db/')
- expect(tar_contents).to match('uploads.tar.gz')
- expect(tar_contents).to match('repositories/')
- expect(tar_contents).to match('builds.tar.gz')
- expect(tar_contents).to match('artifacts.tar.gz')
- expect(tar_contents).to match('lfs.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/)
+ it 'should set correct permissions on the tar contents' do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ )
+ expect(exit_status).to eq(0)
+ expect(tar_contents).to match('db/')
+ expect(tar_contents).to match('uploads.tar.gz')
+ expect(tar_contents).to match('repositories/')
+ expect(tar_contents).to match('builds.tar.gz')
+ expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('registry.tar.gz')
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ end
+
+ it 'should delete temp directories' do
+ temp_dirs = Dir.glob(
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
+ )
+
+ expect(temp_dirs).to be_empty
+ end
+
+ context 'registry disabled' do
+ let(:enable_registry) { false }
+
+ it 'should not create registry.tar.gz' do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar}}
+ )
+ expect(exit_status).to eq(0)
+ expect(tar_contents).not_to match('registry.tar.gz')
+ end
+ end
end
- it 'should delete temp directories' do
- temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}')
- )
+ context 'multiple repository storages' do
+ let(:project_a) { create(:project, repository_storage: 'default') }
+ let(:project_b) { create(:project, repository_storage: 'custom') }
+
+ before do
+ FileUtils.mkdir('tmp/tests/default_storage')
+ FileUtils.mkdir('tmp/tests/custom_storage')
+ storages = {
+ 'default' => 'tmp/tests/default_storage',
+ 'custom' => 'tmp/tests/custom_storage'
+ }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+
+ # Create the projects now, after mocking the settings but before doing the backup
+ project_a
+ project_b
+
+ # We only need a backup of the repositories for this test
+ ENV["SKIP"] = "db,uploads,builds,artifacts,lfs,registry"
+ create_backup
+ end
+
+ after do
+ FileUtils.rm_rf('tmp/tests/default_storage')
+ FileUtils.rm_rf('tmp/tests/custom_storage')
+ FileUtils.rm(@backup_tar)
+ end
- expect(temp_dirs).to be_empty
+ it 'should include repositories in all repository storages' do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} repositories}
+ )
+ expect(exit_status).to eq(0)
+ expect(tar_contents).to match("repositories/#{project_a.path_with_namespace}.bundle")
+ expect(tar_contents).to match("repositories/#{project_b.path_with_namespace}.bundle")
+ end
end
end # backup_create task
@@ -172,7 +231,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -180,6 +239,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -195,6 +255,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
new file mode 100644
index 00000000000..36d03a224e4
--- /dev/null
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:db namespace rake task' do
+ before :all do
+ Rake.application.rake_require 'active_record/railties/databases'
+ Rake.application.rake_require 'tasks/seed_fu'
+ Rake.application.rake_require 'tasks/gitlab/db'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ before do
+ # Stub out db tasks
+ allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
+ end
+
+ describe 'configure' do
+ it 'should invoke db:migrate when schema has already been loaded' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
+ it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
+ it 'should not invoke any other rake tasks during an error' do
+ allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error')
+ # unstub connection so that the database cleaner still works
+ allow(ActiveRecord::Base).to receive(:connection).and_call_original
+ end
+
+ it 'should not invoke seed after a failed schema_load' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
+ allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error')
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error')
+ end
+ end
+
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+end
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 58f45ff8610..69b2b9b6d5b 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -41,11 +41,11 @@ Teaspoon.configure do |config|
suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
# Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
- #suite.javascripts = []
+ # suite.javascripts = []
# You can include your own stylesheets if you want to change how Teaspoon looks.
# Note: Spec related CSS can and should be loaded using fixtures.
- #suite.stylesheets = ["teaspoon"]
+ # suite.stylesheets = ["teaspoon"]
# This suites spec helper, which can require additional support files. This file is loaded before any of your test
# files are loaded.
@@ -62,19 +62,19 @@ Teaspoon.configure do |config|
# Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
# synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
- #suite.hook :fixtures, &proc{}
+ # suite.hook :fixtures, &proc{}
# Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated
- # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
+ # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
# Teaspoon expands all assets to provide more valuable stack traces that reference individual source files.
- #suite.expand_assets = true
+ # suite.expand_assets = true
end
# Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
# be run in the default suite -- but can be focused into a more specific suite.
- #config.suite :targeted do |suite|
+ # config.suite :targeted do |suite|
# suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
- #end
+ # end
# CONSOLE RUNNER SPECIFIC
#
@@ -94,45 +94,45 @@ Teaspoon.configure do |config|
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
# Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- #config.driver = :phantomjs
+ # config.driver = :phantomjs
# Specify additional options for the driver.
#
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
# Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- #config.driver_options = nil
+ # config.driver_options = nil
# Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
# considered a failure. This is to avoid issues that can arise where tests stall.
- #config.driver_timeout = 180
+ # config.driver_timeout = 180
# Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
- #config.server = nil
+ # config.server = nil
# Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
- #config.server_port = nil
+ # config.server_port = nil
# Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
# want to lower this if you know it shouldn't take long to start.
- #config.server_timeout = 20
+ # config.server_timeout = 20
# Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
# several suites, but in environments like CI this may not be desirable.
- #config.fail_fast = true
+ # config.fail_fast = true
# Specify the formatters to use when outputting the results.
# Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
#
# Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity
- #config.formatters = [:dot]
+ # config.formatters = [:dot]
# Specify if you want color output from the formatters.
- #config.color = true
+ # config.color = true
# Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
# remove them, but in verbose applications this may not be desirable.
- #config.suppress_log = false
+ # config.suppress_log = false
# COVERAGE REPORTS / THRESHOLD ASSERTIONS
#
@@ -149,7 +149,7 @@ Teaspoon.configure do |config|
# Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
# on the CLI.
# Set this to "true" or the name of your coverage config.
- #config.use_coverage = nil
+ # config.use_coverage = nil
# You can have multiple coverage configs by passing a name to config.coverage.
# e.g. config.coverage :ci do |coverage|
@@ -158,21 +158,21 @@ Teaspoon.configure do |config|
# Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
#
# Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- #coverage.reports = ["text-summary", "html"]
+ # coverage.reports = ["text-summary", "html"]
# The path that the coverage should be written to - when there's an artifact to write to disk.
# Note: Relative to `config.root`.
- #coverage.output_path = "coverage"
+ # coverage.output_path = "coverage"
# Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
# default excludes assets from vendor, gems and support libraries.
- #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
+ # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
# Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
# aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
- #coverage.statements = nil
- #coverage.functions = nil
- #coverage.branches = nil
- #coverage.lines = nil
+ # coverage.statements = nil
+ # coverage.functions = nil
+ # coverage.branches = nil
+ # coverage.lines = nil
end
end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
new file mode 100644
index 00000000000..cd18d19ef5e
--- /dev/null
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'projects/builds/show' do
+ include Devise::TestHelpers
+
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ before do
+ assign(:build, build)
+ assign(:project, project)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ render
+ end
+
+ it 'does not show retry button' do
+ expect(rendered).not_to have_link('Retry')
+ end
+ end
+
+ context 'when build is not running' do
+ before do
+ build.success!
+ render
+ end
+
+ it 'shows retry button' do
+ expect(rendered).to have_link('Retry')
+ end
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 3600c771075..439da765c2c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -6,29 +6,66 @@ describe EmailsOnPushWorker do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:recipients) { user.email }
+ let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
subject { EmailsOnPushWorker.new }
- before do
- allow(Project).to receive(:find).and_return(project)
- end
-
describe "#perform" do
- it "sends mail" do
- subject.perform(project.id, user.email, data.stringify_keys)
+ context "when there are no errors in sending" do
+ let(:email) { ActionMailer::Base.deliveries.last }
+
+ before { perform }
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include('Change some files')
- expect(email.to).to eq([user.email])
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
end
- it "gracefully handles an input SMTP error" do
- ActionMailer::Base.deliveries.clear
- allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ context "when there is an SMTP error" do
+ before do
+ ActionMailer::Base.deliveries.clear
+ allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ perform
+ end
+
+ it "gracefully handles an input SMTP error" do
+ expect(ActionMailer::Base.deliveries.count).to eq(0)
+ end
+ end
+
+ context "when there are multiple recipients" do
+ let(:recipients) do
+ 1.upto(5).map { |i| user.email.sub('@', "+#{i}@") }.join("\n")
+ end
+
+ before do
+ # This is a hack because we modify the mail object before sending, for efficency,
+ # but the TestMailer adapter just appends the objects to an array. To clone a mail
+ # object, create a new one!
+ # https://github.com/mikel/mail/issues/314#issuecomment-12750108
+ allow_any_instance_of(Mail::TestMailer).to receive(:deliver!).and_wrap_original do |original, mail|
+ original.call(Mail.new(mail.encoded))
+ end
+
+ ActionMailer::Base.deliveries.clear
+ end
- subject.perform(project.id, user.email, data.stringify_keys)
+ it "sends the mail to each of the recipients" do
+ perform
+ expect(ActionMailer::Base.deliveries.count).to eq(5)
+ expect(ActionMailer::Base.deliveries.map(&:to).flatten).to contain_exactly(*recipients.split)
+ end
- expect(ActionMailer::Base.deliveries.count).to eq(0)
+ it "only generates the mail once" do
+ expect(Notify).to receive(:repository_push_email).once.and_call_original
+ expect(Premailer::Rails::CustomizedPremailer).to receive(:new).once.and_call_original
+ perform
+ end
end
end
end
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..7d6668920c0
--- /dev/null
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe ExpireBuildArtifactsWorker do
+ include RepoHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before { build }
+
+ subject! { worker.perform }
+
+ context 'with expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'does expire' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+
+ it 'does remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_falsey
+ end
+
+ it 'does nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).to be_nil
+ end
+ end
+
+ context 'with not yet expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'without expire date' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'for expired artifacts' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'is still expired' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 1abd87d7d33..b5e1fdb8ded 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -9,7 +9,7 @@ describe MergeWorker do
before do
source_project.team << [author, :master]
- source_project.repository.expire_branch_names
+ source_project.repository.expire_branches_cache
end
it 'clears cache of source repo after removing source branch' do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 94ff3457902..20b1a343c27 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -48,6 +48,22 @@ describe PostReceive do
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
+
+ context "gitlab-ci.yml" do
+ subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
+
+ context "creates a Ci::Pipeline for every change" do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
+ end
+
+ context "does not create a Ci::Pipeline" do
+ before { stub_ci_pipeline_yaml_file(nil) }
+
+ it { expect{ subject }.not_to change{ Ci::Pipeline.count } }
+ end
+ end
end
context "webhook" do
@@ -75,6 +91,6 @@ describe PostReceive do
end
def pwd(project)
- File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace)
+ File.join(Gitlab.config.repositories.storages.default, project.path_with_namespace)
end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 7e59bd2fced..5785a6a06ff 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -7,7 +7,6 @@ describe ProjectCacheWorker do
describe '#perform' do
it 'updates project cache data' do
-
expect_any_instance_of(Repository).to receive(:size)
expect_any_instance_of(Repository).to receive(:commit_count)
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 5a03bb77ebd..05e07789dac 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -4,6 +4,26 @@ require 'fileutils'
describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
+ it 'passes when the project has no push events' do
+ project = create(:project_empty_repo, wiki_enabled: false)
+ project.events.destroy_all
+ break_repo(project)
+
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(false)
+ end
+
+ it 'fails when the project has push events and a broken repository' do
+ project = create(:project_empty_repo)
+ create_push_event(project)
+ break_repo(project)
+
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(true)
+ end
+
it 'fails if the wiki repository is broken' do
project = create(:project_empty_repo, wiki_enabled: true)
project.create_wiki
@@ -39,6 +59,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
it 'does not create a wiki if the main repo does not exist at all' do
project = create(:project_empty_repo)
+ create_push_event(project)
FileUtils.rm_rf(project.repository.path_to_repo)
FileUtils.rm_rf(wiki_path(project))
@@ -54,4 +75,12 @@ describe RepositoryCheck::SingleRepositoryWorker do
def wiki_path(project)
project.wiki.repository.path_to_repo
end
+
+ def create_push_event(project)
+ project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ end
+
+ def break_repo(project)
+ FileUtils.rm_rf(File.join(project.repository.path_to_repo, 'objects'))
+ end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 4ef05eb29d2..5f762282b5e 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -14,6 +14,7 @@ describe RepositoryForkWorker do
describe "#perform" do
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
+ project.repository_storage_path,
project.path_with_namespace,
fork_project.namespace.path
).and_return(true)
@@ -25,9 +26,11 @@ describe RepositoryForkWorker do
end
it 'flushes various caches' do
- expect(shell).to receive(:fork_repository).
- with(project.path_with_namespace, fork_project.namespace.path).
- and_return(true)
+ expect(shell).to receive(:fork_repository).with(
+ project.repository_storage_path,
+ project.path_with_namespace,
+ fork_project.namespace.path
+ ).and_return(true)
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
and_call_original
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 6739063543b..f1b1574abf4 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -6,14 +6,28 @@ describe RepositoryImportWorker do
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 })
+ context 'when the import was successful' 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)
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ expect_any_instance_of(Project).to receive(:import_finish)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
+ end
+
+ context 'when the import has failed' do
+ it 'hide the credentials that were used in the import URL' do
+ error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ expect_any_instance_of(Projects::ImportService).to receive(:execute).
+ and_return({ status: :error, message: error })
+
+ subject.perform(project.id)
+
+ expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
+ end
end
end
end
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
index 665ec20f224..801fa31b45d 100644
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ b/spec/workers/stuck_ci_builds_worker_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe StuckCiBuildsWorker do
let!(:build) { create :ci_build }
+ let(:worker) { described_class.new }
subject do
build.reload
@@ -16,13 +17,13 @@ describe StuckCiBuildsWorker do
it 'gets dropped if it was updated over 2 days ago' do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq('failed')
end
it "is still #{status}" do
build.update!(updated_at: 1.minute.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
@@ -36,9 +37,21 @@ describe StuckCiBuildsWorker do
it "is still #{status}" do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
end
+
+ context "for deleted project" do
+ before do
+ build.update!(status: :running, updated_at: 2.days.ago)
+ build.project.update(pending_delete: true)
+ end
+
+ it "does not drop build" do
+ expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ worker.perform
+ end
+ end
end
diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
new file mode 100755
index 00000000000..7ba17766b70
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.scrollTo.js
@@ -0,0 +1,210 @@
+/*!
+ * jQuery.scrollTo
+ * Copyright (c) 2007-2015 Ariel Flesler - aflesler<a>gmail<d>com | http://flesler.blogspot.com
+ * Licensed under MIT
+ * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
+ * @projectDescription Lightweight, cross-browser and highly customizable animated scrolling with jQuery
+ * @author Ariel Flesler
+ * @version 2.1.2
+ */
+;(function(factory) {
+ 'use strict';
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define(['jquery'], factory);
+ } else if (typeof module !== 'undefined' && module.exports) {
+ // CommonJS
+ module.exports = factory(require('jquery'));
+ } else {
+ // Global
+ factory(jQuery);
+ }
+})(function($) {
+ 'use strict';
+
+ var $scrollTo = $.scrollTo = function(target, duration, settings) {
+ return $(window).scrollTo(target, duration, settings);
+ };
+
+ $scrollTo.defaults = {
+ axis:'xy',
+ duration: 0,
+ limit:true
+ };
+
+ function isWin(elem) {
+ return !elem.nodeName ||
+ $.inArray(elem.nodeName.toLowerCase(), ['iframe','#document','html','body']) !== -1;
+ }
+
+ $.fn.scrollTo = function(target, duration, settings) {
+ if (typeof duration === 'object') {
+ settings = duration;
+ duration = 0;
+ }
+ if (typeof settings === 'function') {
+ settings = { onAfter:settings };
+ }
+ if (target === 'max') {
+ target = 9e9;
+ }
+
+ settings = $.extend({}, $scrollTo.defaults, settings);
+ // Speed is still recognized for backwards compatibility
+ duration = duration || settings.duration;
+ // Make sure the settings are given right
+ var queue = settings.queue && settings.axis.length > 1;
+ if (queue) {
+ // Let's keep the overall duration
+ duration /= 2;
+ }
+ settings.offset = both(settings.offset);
+ settings.over = both(settings.over);
+
+ return this.each(function() {
+ // Null target yields nothing, just like jQuery does
+ if (target === null) return;
+
+ var win = isWin(this),
+ elem = win ? this.contentWindow || window : this,
+ $elem = $(elem),
+ targ = target,
+ attr = {},
+ toff;
+
+ switch (typeof targ) {
+ // A number will pass the regex
+ case 'number':
+ case 'string':
+ if (/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)) {
+ targ = both(targ);
+ // We are done
+ break;
+ }
+ // Relative/Absolute selector
+ targ = win ? $(targ) : $(targ, elem);
+ /* falls through */
+ case 'object':
+ if (targ.length === 0) return;
+ // DOMElement / jQuery
+ if (targ.is || targ.style) {
+ // Get the real position of the target
+ toff = (targ = $(targ)).offset();
+ }
+ }
+
+ var offset = $.isFunction(settings.offset) && settings.offset(elem, targ) || settings.offset;
+
+ $.each(settings.axis.split(''), function(i, axis) {
+ var Pos = axis === 'x' ? 'Left' : 'Top',
+ pos = Pos.toLowerCase(),
+ key = 'scroll' + Pos,
+ prev = $elem[key](),
+ max = $scrollTo.max(elem, axis);
+
+ if (toff) {// jQuery / DOMElement
+ attr[key] = toff[pos] + (win ? 0 : prev - $elem.offset()[pos]);
+
+ // If it's a dom element, reduce the margin
+ if (settings.margin) {
+ attr[key] -= parseInt(targ.css('margin'+Pos), 10) || 0;
+ attr[key] -= parseInt(targ.css('border'+Pos+'Width'), 10) || 0;
+ }
+
+ attr[key] += offset[pos] || 0;
+
+ if (settings.over[pos]) {
+ // Scroll to a fraction of its width/height
+ attr[key] += targ[axis === 'x'?'width':'height']() * settings.over[pos];
+ }
+ } else {
+ var val = targ[pos];
+ // Handle percentage values
+ attr[key] = val.slice && val.slice(-1) === '%' ?
+ parseFloat(val) / 100 * max
+ : val;
+ }
+
+ // Number or 'number'
+ if (settings.limit && /^\d+$/.test(attr[key])) {
+ // Check the limits
+ attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max);
+ }
+
+ // Don't waste time animating, if there's no need.
+ if (!i && settings.axis.length > 1) {
+ if (prev === attr[key]) {
+ // No animation needed
+ attr = {};
+ } else if (queue) {
+ // Intermediate animation
+ animate(settings.onAfterFirst);
+ // Don't animate this axis again in the next iteration.
+ attr = {};
+ }
+ }
+ });
+
+ animate(settings.onAfter);
+
+ function animate(callback) {
+ var opts = $.extend({}, settings, {
+ // The queue setting conflicts with animate()
+ // Force it to always be true
+ queue: true,
+ duration: duration,
+ complete: callback && function() {
+ callback.call(elem, targ, settings);
+ }
+ });
+ $elem.animate(attr, opts);
+ }
+ });
+ };
+
+ // Max scrolling position, works on quirks mode
+ // It only fails (not too badly) on IE, quirks mode.
+ $scrollTo.max = function(elem, axis) {
+ var Dim = axis === 'x' ? 'Width' : 'Height',
+ scroll = 'scroll'+Dim;
+
+ if (!isWin(elem))
+ return elem[scroll] - $(elem)[Dim.toLowerCase()]();
+
+ var size = 'client' + Dim,
+ doc = elem.ownerDocument || elem.document,
+ html = doc.documentElement,
+ body = doc.body;
+
+ return Math.max(html[scroll], body[scroll]) - Math.min(html[size], body[size]);
+ };
+
+ function both(val) {
+ return $.isFunction(val) || $.isPlainObject(val) ? val : { top:val, left:val };
+ }
+
+ // Add special hooks so that window scroll properties can be animated
+ $.Tween.propHooks.scrollLeft =
+ $.Tween.propHooks.scrollTop = {
+ get: function(t) {
+ return $(t.elem)[t.prop]();
+ },
+ set: function(t) {
+ var curr = this.get(t);
+ // If interrupt is true and user scrolled, stop animating
+ if (t.options.interrupt && t._last && t._last !== curr) {
+ return $(t.elem).stop();
+ }
+ var next = Math.round(t.now);
+ // Don't waste CPU
+ // Browsers don't render floating point scroll
+ if (curr !== next) {
+ $(t.elem)[t.prop](next);
+ t._last = this.get(t);
+ }
+ }
+ };
+
+ // AMD requirement
+ return $scrollTo;
+});
diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js
new file mode 100644
index 00000000000..3f3f8a0b7f6
--- /dev/null
+++ b/vendor/assets/javascripts/raphael.js
@@ -0,0 +1,8239 @@
+// ┌────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\
+// ├────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\
+// ├────────────────────────────────────────────────────────────────────┤ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\
+// └────────────────────────────────────────────────────────────────────┘ \\
+// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ┌────────────────────────────────────────────────────────────┐ \\
+// │ Eve 0.4.2 - JavaScript Events Library │ \\
+// ├────────────────────────────────────────────────────────────┤ \\
+// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\
+// └────────────────────────────────────────────────────────────┘ \\
+
+(function (glob) {
+ var version = "0.4.2",
+ has = "hasOwnProperty",
+ separator = /[\.\/]/,
+ wildcard = "*",
+ fun = function () {},
+ numsort = function (a, b) {
+ return a - b;
+ },
+ current_event,
+ stop,
+ events = {n: {}},
+ /*\
+ * eve
+ [ method ]
+
+ * Fires event with given `name`, given scope and other parameters.
+
+ > Arguments
+
+ - name (string) name of the *event*, dot (`.`) or slash (`/`) separated
+ - scope (object) context for the event handlers
+ - varargs (...) the rest of arguments will be sent to event handlers
+
+ = (object) array of returned values from the listeners
+ \*/
+ eve = function (name, scope) {
+ name = String(name);
+ var e = events,
+ oldstop = stop,
+ args = Array.prototype.slice.call(arguments, 2),
+ listeners = eve.listeners(name),
+ z = 0,
+ f = false,
+ l,
+ indexed = [],
+ queue = {},
+ out = [],
+ ce = current_event,
+ errors = [];
+ current_event = name;
+ stop = 0;
+ for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) {
+ indexed.push(listeners[i].zIndex);
+ if (listeners[i].zIndex < 0) {
+ queue[listeners[i].zIndex] = listeners[i];
+ }
+ }
+ indexed.sort(numsort);
+ while (indexed[z] < 0) {
+ l = queue[indexed[z++]];
+ out.push(l.apply(scope, args));
+ if (stop) {
+ stop = oldstop;
+ return out;
+ }
+ }
+ for (i = 0; i < ii; i++) {
+ l = listeners[i];
+ if ("zIndex" in l) {
+ if (l.zIndex == indexed[z]) {
+ out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ do {
+ z++;
+ l = queue[indexed[z]];
+ l && out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ } while (l)
+ } else {
+ queue[l.zIndex] = l;
+ }
+ } else {
+ out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ }
+ }
+ stop = oldstop;
+ current_event = ce;
+ return out.length ? out : null;
+ };
+ // Undocumented. Debug only.
+ eve._events = events;
+ /*\
+ * eve.listeners
+ [ method ]
+
+ * Internal method which gives you array of all event handlers that will be triggered by the given `name`.
+
+ > Arguments
+
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated
+
+ = (array) array of event handlers
+ \*/
+ eve.listeners = function (name) {
+ var names = name.split(separator),
+ e = events,
+ item,
+ items,
+ k,
+ i,
+ ii,
+ j,
+ jj,
+ nes,
+ es = [e],
+ out = [];
+ for (i = 0, ii = names.length; i < ii; i++) {
+ nes = [];
+ for (j = 0, jj = es.length; j < jj; j++) {
+ e = es[j].n;
+ items = [e[names[i]], e[wildcard]];
+ k = 2;
+ while (k--) {
+ item = items[k];
+ if (item) {
+ nes.push(item);
+ out = out.concat(item.f || []);
+ }
+ }
+ }
+ es = nes;
+ }
+ return out;
+ };
+
+ /*\
+ * eve.on
+ [ method ]
+ **
+ * Binds given event handler with a given name. You can use wildcards “`*`” for the names:
+ | eve.on("*.under.*", f);
+ | eve("mouse.under.floor"); // triggers f
+ * Use @eve to trigger the listener.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ **
+ = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment.
+ > Example:
+ | eve.on("mouse", eatIt)(2);
+ | eve.on("mouse", scream);
+ | eve.on("mouse", catchIt)(1);
+ * This will ensure that `catchIt()` function will be called before `eatIt()`.
+ *
+ * If you want to put your handler before non-indexed handlers, specify a negative value.
+ * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”.
+ \*/
+ eve.on = function (name, f) {
+ name = String(name);
+ if (typeof f != "function") {
+ return function () {};
+ }
+ var names = name.split(separator),
+ e = events;
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ e = e.n;
+ e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}});
+ }
+ e.f = e.f || [];
+ for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) {
+ return fun;
+ }
+ e.f.push(f);
+ return function (zIndex) {
+ if (+zIndex == +zIndex) {
+ f.zIndex = +zIndex;
+ }
+ };
+ };
+ /*\
+ * eve.f
+ [ method ]
+ **
+ * Returns function that will fire given event with optional arguments.
+ * Arguments that will be passed to the result function will be also
+ * concated to the list of final arguments.
+ | el.onclick = eve.f("click", 1, 2);
+ | eve.on("click", function (a, b, c) {
+ | console.log(a, b, c); // 1, 2, [event object]
+ | });
+ > Arguments
+ - event (string) event name
+ - varargs (…) and any other arguments
+ = (function) possible event handler function
+ \*/
+ eve.f = function (event) {
+ var attrs = [].slice.call(arguments, 1);
+ return function () {
+ eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0)));
+ };
+ };
+ /*\
+ * eve.stop
+ [ method ]
+ **
+ * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing.
+ \*/
+ eve.stop = function () {
+ stop = 1;
+ };
+ /*\
+ * eve.nt
+ [ method ]
+ **
+ * Could be used inside event handler to figure out actual name of the event.
+ **
+ > Arguments
+ **
+ - subname (string) #optional subname of the event
+ **
+ = (string) name of the event, if `subname` is not specified
+ * or
+ = (boolean) `true`, if current event’s name contains `subname`
+ \*/
+ eve.nt = function (subname) {
+ if (subname) {
+ return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event);
+ }
+ return current_event;
+ };
+ /*\
+ * eve.nts
+ [ method ]
+ **
+ * Could be used inside event handler to figure out actual name of the event.
+ **
+ **
+ = (array) names of the event
+ \*/
+ eve.nts = function () {
+ return current_event.split(separator);
+ };
+ /*\
+ * eve.off
+ [ method ]
+ **
+ * Removes given function from the list of event listeners assigned to given name.
+ * If no arguments specified all the events will be cleared.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ \*/
+ /*\
+ * eve.unbind
+ [ method ]
+ **
+ * See @eve.off
+ \*/
+ eve.off = eve.unbind = function (name, f) {
+ if (!name) {
+ eve._events = events = {n: {}};
+ return;
+ }
+ var names = name.split(separator),
+ e,
+ key,
+ splice,
+ i, ii, j, jj,
+ cur = [events];
+ for (i = 0, ii = names.length; i < ii; i++) {
+ for (j = 0; j < cur.length; j += splice.length - 2) {
+ splice = [j, 1];
+ e = cur[j].n;
+ if (names[i] != wildcard) {
+ if (e[names[i]]) {
+ splice.push(e[names[i]]);
+ }
+ } else {
+ for (key in e) if (e[has](key)) {
+ splice.push(e[key]);
+ }
+ }
+ cur.splice.apply(cur, splice);
+ }
+ }
+ for (i = 0, ii = cur.length; i < ii; i++) {
+ e = cur[i];
+ while (e.n) {
+ if (f) {
+ if (e.f) {
+ for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) {
+ e.f.splice(j, 1);
+ break;
+ }
+ !e.f.length && delete e.f;
+ }
+ for (key in e.n) if (e.n[has](key) && e.n[key].f) {
+ var funcs = e.n[key].f;
+ for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) {
+ funcs.splice(j, 1);
+ break;
+ }
+ !funcs.length && delete e.n[key].f;
+ }
+ } else {
+ delete e.f;
+ for (key in e.n) if (e.n[has](key) && e.n[key].f) {
+ delete e.n[key].f;
+ }
+ }
+ e = e.n;
+ }
+ }
+ };
+ /*\
+ * eve.once
+ [ method ]
+ **
+ * Binds given event handler with a given name to only run once then unbind itself.
+ | eve.once("login", f);
+ | eve("login"); // triggers f
+ | eve("login"); // no listeners
+ * Use @eve to trigger the listener.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ **
+ = (function) same return function as @eve.on
+ \*/
+ eve.once = function (name, f) {
+ var f2 = function () {
+ eve.unbind(name, f2);
+ return f.apply(this, arguments);
+ };
+ return eve.on(name, f2);
+ };
+ /*\
+ * eve.version
+ [ property (string) ]
+ **
+ * Current version of the library.
+ \*/
+ eve.version = version;
+ eve.toString = function () {
+ return "You are running Eve " + version;
+ };
+ (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve));
+})(window || this);
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ "Raphaël 2.1.2" - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function (glob, factory) {
+ // AMD support
+ if (typeof define === "function" && define.amd) {
+ // Define as an anonymous module
+ define(["eve"], function( eve ) {
+ return factory(glob, eve);
+ });
+ } else {
+ // Browser globals (glob is window)
+ // Raphael adds itself to window
+ factory(glob, glob.eve || (typeof require == "function" && require('eve')) );
+ }
+}(this, function (window, eve) {
+ /*\
+ * Raphael
+ [ method ]
+ **
+ * Creates a canvas object on which to draw.
+ * You must do this first, as all future calls to drawing methods
+ * from this instance will be bound to this canvas.
+ > Parameters
+ **
+ - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface
+ - width (number)
+ - height (number)
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - x (number)
+ - y (number)
+ - width (number)
+ - height (number)
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add.
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`.
+ = (object) @Paper
+ > Usage
+ | // Each of the following examples create a canvas
+ | // that is 320px wide by 200px high.
+ | // Canvas is created at the viewport’s 10,50 coordinate.
+ | var paper = Raphael(10, 50, 320, 200);
+ | // Canvas is created at the top left corner of the #notepad element
+ | // (or its top right corner in dir="rtl" elements)
+ | var paper = Raphael(document.getElementById("notepad"), 320, 200);
+ | // Same as above
+ | var paper = Raphael("notepad", 320, 200);
+ | // Image dump
+ | var set = Raphael(["notepad", 320, 200, {
+ | type: "rect",
+ | x: 10,
+ | y: 10,
+ | width: 25,
+ | height: 25,
+ | stroke: "#f00"
+ | }, {
+ | type: "text",
+ | x: 30,
+ | y: 40,
+ | text: "Dump"
+ | }]);
+ \*/
+ function R(first) {
+ if (R.is(first, "function")) {
+ return loaded ? first() : eve.on("raphael.DOMload", first);
+ } else if (R.is(first, array)) {
+ return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first);
+ } else {
+ var args = Array.prototype.slice.call(arguments, 0);
+ if (R.is(args[args.length - 1], "function")) {
+ var f = args.pop();
+ return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () {
+ f.call(R._engine.create[apply](R, args));
+ });
+ } else {
+ return R._engine.create[apply](R, arguments);
+ }
+ }
+ }
+ R.version = "2.1.2";
+ R.eve = eve;
+ var loaded,
+ separator = /[, ]+/,
+ elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1},
+ formatrg = /\{(\d+)\}/g,
+ proto = "prototype",
+ has = "hasOwnProperty",
+ g = {
+ doc: document,
+ win: window
+ },
+ oldRaphael = {
+ was: Object.prototype[has].call(g.win, "Raphael"),
+ is: g.win.Raphael
+ },
+ Paper = function () {
+ /*\
+ * Paper.ca
+ [ property (object) ]
+ **
+ * Shortcut for @Paper.customAttributes
+ \*/
+ /*\
+ * Paper.customAttributes
+ [ property (object) ]
+ **
+ * If you have a set of attributes that you would like to represent
+ * as a function of some number you can do it easily with custom attributes:
+ > Usage
+ | paper.customAttributes.hue = function (num) {
+ | num = num % 1;
+ | return {fill: "hsb(" + num + ", 0.75, 1)"};
+ | };
+ | // Custom attribute “hue” will change fill
+ | // to be given hue with fixed saturation and brightness.
+ | // Now you can use it like this:
+ | var c = paper.circle(10, 10, 10).attr({hue: .45});
+ | // or even like this:
+ | c.animate({hue: 1}, 1e3);
+ |
+ | // You could also create custom attribute
+ | // with multiple parameters:
+ | paper.customAttributes.hsb = function (h, s, b) {
+ | return {fill: "hsb(" + [h, s, b].join(",") + ")"};
+ | };
+ | c.attr({hsb: "0.5 .8 1"});
+ | c.animate({hsb: [1, 0, 0.5]}, 1e3);
+ \*/
+ this.ca = this.customAttributes = {};
+ },
+ paperproto,
+ appendChild = "appendChild",
+ apply = "apply",
+ concat = "concat",
+ supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test
+ E = "",
+ S = " ",
+ Str = String,
+ split = "split",
+ events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S),
+ touchMap = {
+ mousedown: "touchstart",
+ mousemove: "touchmove",
+ mouseup: "touchend"
+ },
+ lowerCase = Str.prototype.toLowerCase,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ abs = math.abs,
+ pow = math.pow,
+ PI = math.PI,
+ nu = "number",
+ string = "string",
+ array = "array",
+ toString = "toString",
+ fillString = "fill",
+ objectToString = Object.prototype.toString,
+ paper = {},
+ push = "push",
+ ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i,
+ colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i,
+ isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1},
+ bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
+ round = math.round,
+ setAttribute = "setAttribute",
+ toFloat = parseFloat,
+ toInt = parseInt,
+ upperCase = Str.prototype.toUpperCase,
+ availableAttrs = R._availableAttrs = {
+ "arrow-end": "none",
+ "arrow-start": "none",
+ blur: 0,
+ "clip-rect": "0 0 1e9 1e9",
+ cursor: "default",
+ cx: 0,
+ cy: 0,
+ fill: "#fff",
+ "fill-opacity": 1,
+ font: '10px "Arial"',
+ "font-family": '"Arial"',
+ "font-size": "10",
+ "font-style": "normal",
+ "font-weight": 400,
+ gradient: 0,
+ height: 0,
+ href: "http://raphaeljs.com/",
+ "letter-spacing": 0,
+ opacity: 1,
+ path: "M0,0",
+ r: 0,
+ rx: 0,
+ ry: 0,
+ src: "",
+ stroke: "#000",
+ "stroke-dasharray": "",
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "butt",
+ "stroke-miterlimit": 0,
+ "stroke-opacity": 1,
+ "stroke-width": 1,
+ target: "_blank",
+ "text-anchor": "middle",
+ title: "Raphael",
+ transform: "",
+ width: 0,
+ x: 0,
+ y: 0
+ },
+ availableAnimAttrs = R._availableAnimAttrs = {
+ blur: nu,
+ "clip-rect": "csv",
+ cx: nu,
+ cy: nu,
+ fill: "colour",
+ "fill-opacity": nu,
+ "font-size": nu,
+ height: nu,
+ opacity: nu,
+ path: "path",
+ r: nu,
+ rx: nu,
+ ry: nu,
+ stroke: "colour",
+ "stroke-opacity": nu,
+ "stroke-width": nu,
+ transform: "transform",
+ width: nu,
+ x: nu,
+ y: nu
+ },
+ whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g,
+ commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/,
+ hsrg = {hs: 1, rg: 1},
+ p2s = /,?([achlmqrstvxz]),?/gi,
+ pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
+ tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
+ pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig,
+ radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/,
+ eldata = {},
+ sortByKey = function (a, b) {
+ return a.key - b.key;
+ },
+ sortByNumber = function (a, b) {
+ return toFloat(a) - toFloat(b);
+ },
+ fun = function () {},
+ pipe = function (x) {
+ return x;
+ },
+ rectPath = R._rectPath = function (x, y, w, h, r) {
+ if (r) {
+ return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
+ }
+ return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
+ },
+ ellipsePath = function (x, y, rx, ry) {
+ if (ry == null) {
+ ry = rx;
+ }
+ return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
+ },
+ getPath = R._getPath = {
+ path: function (el) {
+ return el.attr("path");
+ },
+ circle: function (el) {
+ var a = el.attrs;
+ return ellipsePath(a.cx, a.cy, a.r);
+ },
+ ellipse: function (el) {
+ var a = el.attrs;
+ return ellipsePath(a.cx, a.cy, a.rx, a.ry);
+ },
+ rect: function (el) {
+ var a = el.attrs;
+ return rectPath(a.x, a.y, a.width, a.height, a.r);
+ },
+ image: function (el) {
+ var a = el.attrs;
+ return rectPath(a.x, a.y, a.width, a.height);
+ },
+ text: function (el) {
+ var bbox = el._getBBox();
+ return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
+ },
+ set : function(el) {
+ var bbox = el._getBBox();
+ return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
+ }
+ },
+ /*\
+ * Raphael.mapPath
+ [ method ]
+ **
+ * Transform the path string with given matrix.
+ > Parameters
+ - path (string) path string
+ - matrix (object) see @Matrix
+ = (string) transformed path string
+ \*/
+ mapPath = R.mapPath = function (path, matrix) {
+ if (!matrix) {
+ return path;
+ }
+ var x, y, i, j, ii, jj, pathi;
+ path = path2curve(path);
+ for (i = 0, ii = path.length; i < ii; i++) {
+ pathi = path[i];
+ for (j = 1, jj = pathi.length; j < jj; j += 2) {
+ x = matrix.x(pathi[j], pathi[j + 1]);
+ y = matrix.y(pathi[j], pathi[j + 1]);
+ pathi[j] = x;
+ pathi[j + 1] = y;
+ }
+ }
+ return path;
+ };
+
+ R._g = g;
+ /*\
+ * Raphael.type
+ [ property (string) ]
+ **
+ * Can be “SVG”, “VML” or empty, depending on browser support.
+ \*/
+ R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
+ if (R.type == "VML") {
+ var d = g.doc.createElement("div"),
+ b;
+ d.innerHTML = '<v:shape adj="1"/>';
+ b = d.firstChild;
+ b.style.behavior = "url(#default#VML)";
+ if (!(b && typeof b.adj == "object")) {
+ return (R.type = E);
+ }
+ d = null;
+ }
+ /*\
+ * Raphael.svg
+ [ property (boolean) ]
+ **
+ * `true` if browser supports SVG.
+ \*/
+ /*\
+ * Raphael.vml
+ [ property (boolean) ]
+ **
+ * `true` if browser supports VML.
+ \*/
+ R.svg = !(R.vml = R.type == "VML");
+ R._Paper = Paper;
+ /*\
+ * Raphael.fn
+ [ property (object) ]
+ **
+ * You can add your own method to the canvas. For example if you want to draw a pie chart,
+ * you can create your own pie chart function and ship it as a Raphaël plugin. To do this
+ * you need to extend the `Raphael.fn` object. You should modify the `fn` object before a
+ * Raphaël instance is created, otherwise it will take no effect. Please note that the
+ * ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to
+ * ensure any namespacing ensures proper context.
+ > Usage
+ | Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
+ | return this.path( ... );
+ | };
+ | // or create namespace
+ | Raphael.fn.mystuff = {
+ | arrow: function () {…},
+ | star: function () {…},
+ | // etc…
+ | };
+ | var paper = Raphael(10, 10, 630, 480);
+ | // then use it
+ | paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"});
+ | paper.mystuff.arrow();
+ | paper.mystuff.star();
+ \*/
+ R.fn = paperproto = Paper.prototype = R.prototype;
+ R._id = 0;
+ R._oid = 0;
+ /*\
+ * Raphael.is
+ [ method ]
+ **
+ * Handful of replacements for `typeof` operator.
+ > Parameters
+ - o (…) any object or primitive
+ - type (string) name of the type, i.e. “string”, “function”, “number”, etc.
+ = (boolean) is given value is of given type
+ \*/
+ R.is = function (o, type) {
+ type = lowerCase.call(type);
+ if (type == "finite") {
+ return !isnan[has](+o);
+ }
+ if (type == "array") {
+ return o instanceof Array;
+ }
+ return (type == "null" && o === null) ||
+ (type == typeof o && o !== null) ||
+ (type == "object" && o === Object(o)) ||
+ (type == "array" && Array.isArray && Array.isArray(o)) ||
+ objectToString.call(o).slice(8, -1).toLowerCase() == type;
+ };
+
+ function clone(obj) {
+ if (typeof obj == "function" || Object(obj) !== obj) {
+ return obj;
+ }
+ var res = new obj.constructor;
+ for (var key in obj) if (obj[has](key)) {
+ res[key] = clone(obj[key]);
+ }
+ return res;
+ }
+
+ /*\
+ * Raphael.angle
+ [ method ]
+ **
+ * Returns angle between two or three points
+ > Parameters
+ - x1 (number) x coord of first point
+ - y1 (number) y coord of first point
+ - x2 (number) x coord of second point
+ - y2 (number) y coord of second point
+ - x3 (number) #optional x coord of third point
+ - y3 (number) #optional y coord of third point
+ = (number) angle in degrees.
+ \*/
+ R.angle = function (x1, y1, x2, y2, x3, y3) {
+ if (x3 == null) {
+ var x = x1 - x2,
+ y = y1 - y2;
+ if (!x && !y) {
+ return 0;
+ }
+ return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360;
+ } else {
+ return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3);
+ }
+ };
+ /*\
+ * Raphael.rad
+ [ method ]
+ **
+ * Transform angle to radians
+ > Parameters
+ - deg (number) angle in degrees
+ = (number) angle in radians.
+ \*/
+ R.rad = function (deg) {
+ return deg % 360 * PI / 180;
+ };
+ /*\
+ * Raphael.deg
+ [ method ]
+ **
+ * Transform angle to degrees
+ > Parameters
+ - rad (number) angle in radians
+ = (number) angle in degrees.
+ \*/
+ R.deg = function (rad) {
+ return Math.round ((rad * 180 / PI% 360)* 1000) / 1000;
+ };
+ /*\
+ * Raphael.snapTo
+ [ method ]
+ **
+ * Snaps given value to given grid.
+ > Parameters
+ - values (array|number) given array of values or step of the grid
+ - value (number) value to adjust
+ - tolerance (number) #optional tolerance for snapping. Default is `10`.
+ = (number) adjusted value.
+ \*/
+ R.snapTo = function (values, value, tolerance) {
+ tolerance = R.is(tolerance, "finite") ? tolerance : 10;
+ if (R.is(values, array)) {
+ var i = values.length;
+ while (i--) if (abs(values[i] - value) <= tolerance) {
+ return values[i];
+ }
+ } else {
+ values = +values;
+ var rem = value % values;
+ if (rem < tolerance) {
+ return value - rem;
+ }
+ if (rem > values - tolerance) {
+ return value - rem + values;
+ }
+ }
+ return value;
+ };
+
+ /*\
+ * Raphael.createUUID
+ [ method ]
+ **
+ * Returns RFC4122, version 4 ID
+ \*/
+ var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) {
+ return function () {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase();
+ };
+ })(/[xy]/g, function (c) {
+ var r = math.random() * 16 | 0,
+ v = c == "x" ? r : (r & 3 | 8);
+ return v.toString(16);
+ });
+
+ /*\
+ * Raphael.setWindow
+ [ method ]
+ **
+ * Used when you need to draw in `&lt;iframe>`. Switched window to the iframe one.
+ > Parameters
+ - newwin (window) new window object
+ \*/
+ R.setWindow = function (newwin) {
+ eve("raphael.setWindow", R, g.win, newwin);
+ g.win = newwin;
+ g.doc = g.win.document;
+ if (R._engine.initWin) {
+ R._engine.initWin(g.win);
+ }
+ };
+ var toHex = function (color) {
+ if (R.vml) {
+ // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/
+ var trim = /^\s+|\s+$/g;
+ var bod;
+ try {
+ var docum = new ActiveXObject("htmlfile");
+ docum.write("<body>");
+ docum.close();
+ bod = docum.body;
+ } catch(e) {
+ bod = createPopup().document.body;
+ }
+ var range = bod.createTextRange();
+ toHex = cacher(function (color) {
+ try {
+ bod.style.color = Str(color).replace(trim, E);
+ var value = range.queryCommandValue("ForeColor");
+ value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16);
+ return "#" + ("000000" + value.toString(16)).slice(-6);
+ } catch(e) {
+ return "none";
+ }
+ });
+ } else {
+ var i = g.doc.createElement("i");
+ i.title = "Rapha\xebl Colour Picker";
+ i.style.display = "none";
+ g.doc.body.appendChild(i);
+ toHex = cacher(function (color) {
+ i.style.color = color;
+ return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color");
+ });
+ }
+ return toHex(color);
+ },
+ hsbtoString = function () {
+ return "hsb(" + [this.h, this.s, this.b] + ")";
+ },
+ hsltoString = function () {
+ return "hsl(" + [this.h, this.s, this.l] + ")";
+ },
+ rgbtoString = function () {
+ return this.hex;
+ },
+ prepareRGB = function (r, g, b) {
+ if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) {
+ b = r.b;
+ g = r.g;
+ r = r.r;
+ }
+ if (g == null && R.is(r, string)) {
+ var clr = R.getRGB(r);
+ r = clr.r;
+ g = clr.g;
+ b = clr.b;
+ }
+ if (r > 1 || g > 1 || b > 1) {
+ r /= 255;
+ g /= 255;
+ b /= 255;
+ }
+
+ return [r, g, b];
+ },
+ packageRGB = function (r, g, b, o) {
+ r *= 255;
+ g *= 255;
+ b *= 255;
+ var rgb = {
+ r: r,
+ g: g,
+ b: b,
+ hex: R.rgb(r, g, b),
+ toString: rgbtoString
+ };
+ R.is(o, "finite") && (rgb.opacity = o);
+ return rgb;
+ };
+
+ /*\
+ * Raphael.color
+ [ method ]
+ **
+ * Parses the color string and returns object with all values for the given color.
+ > Parameters
+ - clr (string) color string in one of the supported formats (see @Raphael.getRGB)
+ = (object) Combined RGB & HSB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••,
+ o error (boolean) `true` if string can’t be parsed,
+ o h (number) hue,
+ o s (number) saturation,
+ o v (number) value (brightness),
+ o l (number) lightness
+ o }
+ \*/
+ R.color = function (clr) {
+ var rgb;
+ if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) {
+ rgb = R.hsb2rgb(clr);
+ clr.r = rgb.r;
+ clr.g = rgb.g;
+ clr.b = rgb.b;
+ clr.hex = rgb.hex;
+ } else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) {
+ rgb = R.hsl2rgb(clr);
+ clr.r = rgb.r;
+ clr.g = rgb.g;
+ clr.b = rgb.b;
+ clr.hex = rgb.hex;
+ } else {
+ if (R.is(clr, "string")) {
+ clr = R.getRGB(clr);
+ }
+ if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) {
+ rgb = R.rgb2hsl(clr);
+ clr.h = rgb.h;
+ clr.s = rgb.s;
+ clr.l = rgb.l;
+ rgb = R.rgb2hsb(clr);
+ clr.v = rgb.b;
+ } else {
+ clr = {hex: "none"};
+ clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1;
+ }
+ }
+ clr.toString = rgbtoString;
+ return clr;
+ };
+ /*\
+ * Raphael.hsb2rgb
+ [ method ]
+ **
+ * Converts HSB values to RGB object.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - v (number) value or brightness
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••
+ o }
+ \*/
+ R.hsb2rgb = function (h, s, v, o) {
+ if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) {
+ v = h.b;
+ s = h.s;
+ o = h.o;
+ h = h.h;
+ }
+ h *= 360;
+ var R, G, B, X, C;
+ h = (h % 360) / 60;
+ C = v * s;
+ X = C * (1 - abs(h % 2 - 1));
+ R = G = B = v - C;
+
+ h = ~~h;
+ R += [C, X, 0, 0, X, C][h];
+ G += [X, C, C, X, 0, 0][h];
+ B += [0, 0, X, C, C, X][h];
+ return packageRGB(R, G, B, o);
+ };
+ /*\
+ * Raphael.hsl2rgb
+ [ method ]
+ **
+ * Converts HSL values to RGB object.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - l (number) luminosity
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••
+ o }
+ \*/
+ R.hsl2rgb = function (h, s, l, o) {
+ if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) {
+ l = h.l;
+ s = h.s;
+ h = h.h;
+ }
+ if (h > 1 || s > 1 || l > 1) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ }
+ h *= 360;
+ var R, G, B, X, C;
+ h = (h % 360) / 60;
+ C = 2 * s * (l < .5 ? l : 1 - l);
+ X = C * (1 - abs(h % 2 - 1));
+ R = G = B = l - C / 2;
+
+ h = ~~h;
+ R += [C, X, 0, 0, X, C][h];
+ G += [X, C, C, X, 0, 0][h];
+ B += [0, 0, X, C, C, X][h];
+ return packageRGB(R, G, B, o);
+ };
+ /*\
+ * Raphael.rgb2hsb
+ [ method ]
+ **
+ * Converts RGB values to HSB object.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (object) HSB object in format:
+ o {
+ o h (number) hue
+ o s (number) saturation
+ o b (number) brightness
+ o }
+ \*/
+ R.rgb2hsb = function (r, g, b) {
+ b = prepareRGB(r, g, b);
+ r = b[0];
+ g = b[1];
+ b = b[2];
+
+ var H, S, V, C;
+ V = mmax(r, g, b);
+ C = V - mmin(r, g, b);
+ H = (C == 0 ? null :
+ V == r ? (g - b) / C :
+ V == g ? (b - r) / C + 2 :
+ (r - g) / C + 4
+ );
+ H = ((H + 360) % 6) * 60 / 360;
+ S = C == 0 ? 0 : C / V;
+ return {h: H, s: S, b: V, toString: hsbtoString};
+ };
+ /*\
+ * Raphael.rgb2hsl
+ [ method ]
+ **
+ * Converts RGB values to HSL object.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (object) HSL object in format:
+ o {
+ o h (number) hue
+ o s (number) saturation
+ o l (number) luminosity
+ o }
+ \*/
+ R.rgb2hsl = function (r, g, b) {
+ b = prepareRGB(r, g, b);
+ r = b[0];
+ g = b[1];
+ b = b[2];
+
+ var H, S, L, M, m, C;
+ M = mmax(r, g, b);
+ m = mmin(r, g, b);
+ C = M - m;
+ H = (C == 0 ? null :
+ M == r ? (g - b) / C :
+ M == g ? (b - r) / C + 2 :
+ (r - g) / C + 4);
+ H = ((H + 360) % 6) * 60 / 360;
+ L = (M + m) / 2;
+ S = (C == 0 ? 0 :
+ L < .5 ? C / (2 * L) :
+ C / (2 - 2 * L));
+ return {h: H, s: S, l: L, toString: hsltoString};
+ };
+ R._path2string = function () {
+ return this.join(",").replace(p2s, "$1");
+ };
+ function repush(array, item) {
+ for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) {
+ return array.push(array.splice(i, 1)[0]);
+ }
+ }
+ function cacher(f, scope, postprocessor) {
+ function newf() {
+ var arg = Array.prototype.slice.call(arguments, 0),
+ args = arg.join("\u2400"),
+ cache = newf.cache = newf.cache || {},
+ count = newf.count = newf.count || [];
+ if (cache[has](args)) {
+ repush(count, args);
+ return postprocessor ? postprocessor(cache[args]) : cache[args];
+ }
+ count.length >= 1e3 && delete cache[count.shift()];
+ count.push(args);
+ cache[args] = f[apply](scope, arg);
+ return postprocessor ? postprocessor(cache[args]) : cache[args];
+ }
+ return newf;
+ }
+
+ var preload = R._preload = function (src, f) {
+ var img = g.doc.createElement("img");
+ img.style.cssText = "position:absolute;left:-9999em;top:-9999em";
+ img.onload = function () {
+ f.call(this);
+ this.onload = null;
+ g.doc.body.removeChild(this);
+ };
+ img.onerror = function () {
+ g.doc.body.removeChild(this);
+ };
+ g.doc.body.appendChild(img);
+ img.src = src;
+ };
+
+ function clrToString() {
+ return this.hex;
+ }
+
+ /*\
+ * Raphael.getRGB
+ [ method ]
+ **
+ * Parses colour string as RGB object
+ > Parameters
+ - colour (string) colour string in one of formats:
+ # <ul>
+ # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
+ # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
+ # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
+ # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
+ # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
+ # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
+ # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsl(•••, •••, •••) — same as hsb</li>
+ # <li>hsl(•••%, •••%, •••%) — same as hsb</li>
+ # </ul>
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue
+ o hex (string) color in HTML/CSS format: #••••••,
+ o error (boolean) true if string can’t be parsed
+ o }
+ \*/
+ R.getRGB = cacher(function (colour) {
+ if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) {
+ return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
+ }
+ if (colour == "none") {
+ return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString};
+ }
+ !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour));
+ var res,
+ red,
+ green,
+ blue,
+ opacity,
+ t,
+ values,
+ rgb = colour.match(colourRegExp);
+ if (rgb) {
+ if (rgb[2]) {
+ blue = toInt(rgb[2].substring(5), 16);
+ green = toInt(rgb[2].substring(3, 5), 16);
+ red = toInt(rgb[2].substring(1, 3), 16);
+ }
+ if (rgb[3]) {
+ blue = toInt((t = rgb[3].charAt(3)) + t, 16);
+ green = toInt((t = rgb[3].charAt(2)) + t, 16);
+ red = toInt((t = rgb[3].charAt(1)) + t, 16);
+ }
+ if (rgb[4]) {
+ values = rgb[4][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ }
+ if (rgb[5]) {
+ values = rgb[5][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
+ rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ return R.hsb2rgb(red, green, blue, opacity);
+ }
+ if (rgb[6]) {
+ values = rgb[6][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
+ rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ return R.hsl2rgb(red, green, blue, opacity);
+ }
+ rgb = {r: red, g: green, b: blue, toString: clrToString};
+ rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1);
+ R.is(opacity, "finite") && (rgb.opacity = opacity);
+ return rgb;
+ }
+ return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
+ }, R);
+ /*\
+ * Raphael.hsb
+ [ method ]
+ **
+ * Converts HSB values to hex representation of the colour.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - b (number) value or brightness
+ = (string) hex representation of the colour.
+ \*/
+ R.hsb = cacher(function (h, s, b) {
+ return R.hsb2rgb(h, s, b).hex;
+ });
+ /*\
+ * Raphael.hsl
+ [ method ]
+ **
+ * Converts HSL values to hex representation of the colour.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - l (number) luminosity
+ = (string) hex representation of the colour.
+ \*/
+ R.hsl = cacher(function (h, s, l) {
+ return R.hsl2rgb(h, s, l).hex;
+ });
+ /*\
+ * Raphael.rgb
+ [ method ]
+ **
+ * Converts RGB values to hex representation of the colour.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (string) hex representation of the colour.
+ \*/
+ R.rgb = cacher(function (r, g, b) {
+ return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1);
+ });
+ /*\
+ * Raphael.getColor
+ [ method ]
+ **
+ * On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset
+ > Parameters
+ - value (number) #optional brightness, default is `0.75`
+ = (string) hex representation of the colour.
+ \*/
+ R.getColor = function (value) {
+ var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75},
+ rgb = this.hsb2rgb(start.h, start.s, start.b);
+ start.h += .075;
+ if (start.h > 1) {
+ start.h = 0;
+ start.s -= .2;
+ start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b});
+ }
+ return rgb.hex;
+ };
+ /*\
+ * Raphael.getColor.reset
+ [ method ]
+ **
+ * Resets spectrum position for @Raphael.getColor back to red.
+ \*/
+ R.getColor.reset = function () {
+ delete this.start;
+ };
+
+ // http://schepers.cc/getting-to-the-point
+ function catmullRom2bezier(crp, z) {
+ var d = [];
+ for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) {
+ var p = [
+ {x: +crp[i - 2], y: +crp[i - 1]},
+ {x: +crp[i], y: +crp[i + 1]},
+ {x: +crp[i + 2], y: +crp[i + 3]},
+ {x: +crp[i + 4], y: +crp[i + 5]}
+ ];
+ if (z) {
+ if (!i) {
+ p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]};
+ } else if (iLen - 4 == i) {
+ p[3] = {x: +crp[0], y: +crp[1]};
+ } else if (iLen - 2 == i) {
+ p[2] = {x: +crp[0], y: +crp[1]};
+ p[3] = {x: +crp[2], y: +crp[3]};
+ }
+ } else {
+ if (iLen - 4 == i) {
+ p[3] = p[2];
+ } else if (!i) {
+ p[0] = {x: +crp[i], y: +crp[i + 1]};
+ }
+ }
+ d.push(["C",
+ (-p[0].x + 6 * p[1].x + p[2].x) / 6,
+ (-p[0].y + 6 * p[1].y + p[2].y) / 6,
+ (p[1].x + 6 * p[2].x - p[3].x) / 6,
+ (p[1].y + 6*p[2].y - p[3].y) / 6,
+ p[2].x,
+ p[2].y
+ ]);
+ }
+
+ return d;
+ }
+ /*\
+ * Raphael.parsePathString
+ [ method ]
+ **
+ * Utility method
+ **
+ * Parses given path string into an array of arrays of path segments.
+ > Parameters
+ - pathString (string|array) path string or array of segments (in the last case it will be returned straight away)
+ = (array) array of segments.
+ \*/
+ R.parsePathString = function (pathString) {
+ if (!pathString) {
+ return null;
+ }
+ var pth = paths(pathString);
+ if (pth.arr) {
+ return pathClone(pth.arr);
+ }
+
+ var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0},
+ data = [];
+ if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption
+ data = pathClone(pathString);
+ }
+ if (!data.length) {
+ Str(pathString).replace(pathCommand, function (a, b, c) {
+ var params = [],
+ name = b.toLowerCase();
+ c.replace(pathValues, function (a, b) {
+ b && params.push(+b);
+ });
+ if (name == "m" && params.length > 2) {
+ data.push([b][concat](params.splice(0, 2)));
+ name = "l";
+ b = b == "m" ? "l" : "L";
+ }
+ if (name == "r") {
+ data.push([b][concat](params));
+ } else while (params.length >= paramCounts[name]) {
+ data.push([b][concat](params.splice(0, paramCounts[name])));
+ if (!paramCounts[name]) {
+ break;
+ }
+ }
+ });
+ }
+ data.toString = R._path2string;
+ pth.arr = pathClone(data);
+ return data;
+ };
+ /*\
+ * Raphael.parseTransformString
+ [ method ]
+ **
+ * Utility method
+ **
+ * Parses given path string into an array of transformations.
+ > Parameters
+ - TString (string|array) transform string or array of transformations (in the last case it will be returned straight away)
+ = (array) array of transformations.
+ \*/
+ R.parseTransformString = cacher(function (TString) {
+ if (!TString) {
+ return null;
+ }
+ var paramCounts = {r: 3, s: 4, t: 2, m: 6},
+ data = [];
+ if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption
+ data = pathClone(TString);
+ }
+ if (!data.length) {
+ Str(TString).replace(tCommand, function (a, b, c) {
+ var params = [],
+ name = lowerCase.call(b);
+ c.replace(pathValues, function (a, b) {
+ b && params.push(+b);
+ });
+ data.push([b][concat](params));
+ });
+ }
+ data.toString = R._path2string;
+ return data;
+ });
+ // PATHS
+ var paths = function (ps) {
+ var p = paths.ps = paths.ps || {};
+ if (p[ps]) {
+ p[ps].sleep = 100;
+ } else {
+ p[ps] = {
+ sleep: 100
+ };
+ }
+ setTimeout(function () {
+ for (var key in p) if (p[has](key) && key != ps) {
+ p[key].sleep--;
+ !p[key].sleep && delete p[key];
+ }
+ });
+ return p[ps];
+ };
+ /*\
+ * Raphael.findDotsAtSegment
+ [ method ]
+ **
+ * Utility method
+ **
+ * Find dot coordinates on the given cubic bezier curve at the given t.
+ > Parameters
+ - p1x (number) x of the first point of the curve
+ - p1y (number) y of the first point of the curve
+ - c1x (number) x of the first anchor of the curve
+ - c1y (number) y of the first anchor of the curve
+ - c2x (number) x of the second anchor of the curve
+ - c2y (number) y of the second anchor of the curve
+ - p2x (number) x of the second point of the curve
+ - p2y (number) y of the second point of the curve
+ - t (number) position on the curve (0..1)
+ = (object) point information in format:
+ o {
+ o x: (number) x coordinate of the point
+ o y: (number) y coordinate of the point
+ o m: {
+ o x: (number) x coordinate of the left anchor
+ o y: (number) y coordinate of the left anchor
+ o }
+ o n: {
+ o x: (number) x coordinate of the right anchor
+ o y: (number) y coordinate of the right anchor
+ o }
+ o start: {
+ o x: (number) x coordinate of the start of the curve
+ o y: (number) y coordinate of the start of the curve
+ o }
+ o end: {
+ o x: (number) x coordinate of the end of the curve
+ o y: (number) y coordinate of the end of the curve
+ o }
+ o alpha: (number) angle of the curve derivative at the point
+ o }
+ \*/
+ R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+ var t1 = 1 - t,
+ t13 = pow(t1, 3),
+ t12 = pow(t1, 2),
+ t2 = t * t,
+ t3 = t2 * t,
+ x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x,
+ y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y,
+ mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x),
+ my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y),
+ nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x),
+ ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y),
+ ax = t1 * p1x + t * c1x,
+ ay = t1 * p1y + t * c1y,
+ cx = t1 * c2x + t * p2x,
+ cy = t1 * c2y + t * p2y,
+ alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI);
+ (mx > nx || my < ny) && (alpha += 180);
+ return {
+ x: x,
+ y: y,
+ m: {x: mx, y: my},
+ n: {x: nx, y: ny},
+ start: {x: ax, y: ay},
+ end: {x: cx, y: cy},
+ alpha: alpha
+ };
+ };
+ /*\
+ * Raphael.bezierBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Return bounding box of a given cubic bezier curve
+ > Parameters
+ - p1x (number) x of the first point of the curve
+ - p1y (number) y of the first point of the curve
+ - c1x (number) x of the first anchor of the curve
+ - c1y (number) y of the first anchor of the curve
+ - c2x (number) x of the second anchor of the curve
+ - c2y (number) y of the second anchor of the curve
+ - p2x (number) x of the second point of the curve
+ - p2y (number) y of the second point of the curve
+ * or
+ - bez (array) array of six points for bezier curve
+ = (object) point information in format:
+ o {
+ o min: {
+ o x: (number) x coordinate of the left point
+ o y: (number) y coordinate of the top point
+ o }
+ o max: {
+ o x: (number) x coordinate of the right point
+ o y: (number) y coordinate of the bottom point
+ o }
+ o }
+ \*/
+ R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+ if (!R.is(p1x, "array")) {
+ p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y];
+ }
+ var bbox = curveDim.apply(null, p1x);
+ return {
+ x: bbox.min.x,
+ y: bbox.min.y,
+ x2: bbox.max.x,
+ y2: bbox.max.y,
+ width: bbox.max.x - bbox.min.x,
+ height: bbox.max.y - bbox.min.y
+ };
+ };
+ /*\
+ * Raphael.isPointInsideBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if given point is inside bounding boxes.
+ > Parameters
+ - bbox (string) bounding box
+ - x (string) x coordinate of the point
+ - y (string) y coordinate of the point
+ = (boolean) `true` if point inside
+ \*/
+ R.isPointInsideBBox = function (bbox, x, y) {
+ return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2;
+ };
+ /*\
+ * Raphael.isBBoxIntersect
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if two bounding boxes intersect
+ > Parameters
+ - bbox1 (string) first bounding box
+ - bbox2 (string) second bounding box
+ = (boolean) `true` if they intersect
+ \*/
+ R.isBBoxIntersect = function (bbox1, bbox2) {
+ var i = R.isPointInsideBBox;
+ return i(bbox2, bbox1.x, bbox1.y)
+ || i(bbox2, bbox1.x2, bbox1.y)
+ || i(bbox2, bbox1.x, bbox1.y2)
+ || i(bbox2, bbox1.x2, bbox1.y2)
+ || i(bbox1, bbox2.x, bbox2.y)
+ || i(bbox1, bbox2.x2, bbox2.y)
+ || i(bbox1, bbox2.x, bbox2.y2)
+ || i(bbox1, bbox2.x2, bbox2.y2)
+ || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x)
+ && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y);
+ };
+ function base3(t, p1, p2, p3, p4) {
+ var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
+ t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
+ return t * t2 - 3 * p1 + 3 * p2;
+ }
+ function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) {
+ if (z == null) {
+ z = 1;
+ }
+ z = z > 1 ? 1 : z < 0 ? 0 : z;
+ var z2 = z / 2,
+ n = 12,
+ Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816],
+ Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472],
+ sum = 0;
+ for (var i = 0; i < n; i++) {
+ var ct = z2 * Tvalues[i] + z2,
+ xbase = base3(ct, x1, x2, x3, x4),
+ ybase = base3(ct, y1, y2, y3, y4),
+ comb = xbase * xbase + ybase * ybase;
+ sum += Cvalues[i] * math.sqrt(comb);
+ }
+ return z2 * sum;
+ }
+ function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) {
+ if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) {
+ return;
+ }
+ var t = 1,
+ step = t / 2,
+ t2 = t - step,
+ l,
+ e = .01;
+ l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ while (abs(l - ll) > e) {
+ step /= 2;
+ t2 += (l < ll ? 1 : -1) * step;
+ l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ }
+ return t2;
+ }
+ function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
+ if (
+ mmax(x1, x2) < mmin(x3, x4) ||
+ mmin(x1, x2) > mmax(x3, x4) ||
+ mmax(y1, y2) < mmin(y3, y4) ||
+ mmin(y1, y2) > mmax(y3, y4)
+ ) {
+ return;
+ }
+ var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
+ ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
+ denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
+
+ if (!denominator) {
+ return;
+ }
+ var px = nx / denominator,
+ py = ny / denominator,
+ px2 = +px.toFixed(2),
+ py2 = +py.toFixed(2);
+ if (
+ px2 < +mmin(x1, x2).toFixed(2) ||
+ px2 > +mmax(x1, x2).toFixed(2) ||
+ px2 < +mmin(x3, x4).toFixed(2) ||
+ px2 > +mmax(x3, x4).toFixed(2) ||
+ py2 < +mmin(y1, y2).toFixed(2) ||
+ py2 > +mmax(y1, y2).toFixed(2) ||
+ py2 < +mmin(y3, y4).toFixed(2) ||
+ py2 > +mmax(y3, y4).toFixed(2)
+ ) {
+ return;
+ }
+ return {x: px, y: py};
+ }
+ function inter(bez1, bez2) {
+ return interHelper(bez1, bez2);
+ }
+ function interCount(bez1, bez2) {
+ return interHelper(bez1, bez2, 1);
+ }
+ function interHelper(bez1, bez2, justCount) {
+ var bbox1 = R.bezierBBox(bez1),
+ bbox2 = R.bezierBBox(bez2);
+ if (!R.isBBoxIntersect(bbox1, bbox2)) {
+ return justCount ? 0 : [];
+ }
+ var l1 = bezlen.apply(0, bez1),
+ l2 = bezlen.apply(0, bez2),
+ n1 = mmax(~~(l1 / 5), 1),
+ n2 = mmax(~~(l2 / 5), 1),
+ dots1 = [],
+ dots2 = [],
+ xy = {},
+ res = justCount ? 0 : [];
+ for (var i = 0; i < n1 + 1; i++) {
+ var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1));
+ dots1.push({x: p.x, y: p.y, t: i / n1});
+ }
+ for (i = 0; i < n2 + 1; i++) {
+ p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2));
+ dots2.push({x: p.x, y: p.y, t: i / n2});
+ }
+ for (i = 0; i < n1; i++) {
+ for (var j = 0; j < n2; j++) {
+ var di = dots1[i],
+ di1 = dots1[i + 1],
+ dj = dots2[j],
+ dj1 = dots2[j + 1],
+ ci = abs(di1.x - di.x) < .001 ? "y" : "x",
+ cj = abs(dj1.x - dj.x) < .001 ? "y" : "x",
+ is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
+ if (is) {
+ if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) {
+ continue;
+ }
+ xy[is.x.toFixed(4)] = is.y.toFixed(4);
+ var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t),
+ t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
+ if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) {
+ if (justCount) {
+ res++;
+ } else {
+ res.push({
+ x: is.x,
+ y: is.y,
+ t1: mmin(t1, 1),
+ t2: mmin(t2, 1)
+ });
+ }
+ }
+ }
+ }
+ }
+ return res;
+ }
+ /*\
+ * Raphael.pathIntersection
+ [ method ]
+ **
+ * Utility method
+ **
+ * Finds intersections of two paths
+ > Parameters
+ - path1 (string) path string
+ - path2 (string) path string
+ = (array) dots of intersection
+ o [
+ o {
+ o x: (number) x coordinate of the point
+ o y: (number) y coordinate of the point
+ o t1: (number) t value for segment of path1
+ o t2: (number) t value for segment of path2
+ o segment1: (number) order number for segment of path1
+ o segment2: (number) order number for segment of path2
+ o bez1: (array) eight coordinates representing beziér curve for the segment of path1
+ o bez2: (array) eight coordinates representing beziér curve for the segment of path2
+ o }
+ o ]
+ \*/
+ R.pathIntersection = function (path1, path2) {
+ return interPathHelper(path1, path2);
+ };
+ R.pathIntersectionNumber = function (path1, path2) {
+ return interPathHelper(path1, path2, 1);
+ };
+ function interPathHelper(path1, path2, justCount) {
+ path1 = R._path2curve(path1);
+ path2 = R._path2curve(path2);
+ var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2,
+ res = justCount ? 0 : [];
+ for (var i = 0, ii = path1.length; i < ii; i++) {
+ var pi = path1[i];
+ if (pi[0] == "M") {
+ x1 = x1m = pi[1];
+ y1 = y1m = pi[2];
+ } else {
+ if (pi[0] == "C") {
+ bez1 = [x1, y1].concat(pi.slice(1));
+ x1 = bez1[6];
+ y1 = bez1[7];
+ } else {
+ bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m];
+ x1 = x1m;
+ y1 = y1m;
+ }
+ for (var j = 0, jj = path2.length; j < jj; j++) {
+ var pj = path2[j];
+ if (pj[0] == "M") {
+ x2 = x2m = pj[1];
+ y2 = y2m = pj[2];
+ } else {
+ if (pj[0] == "C") {
+ bez2 = [x2, y2].concat(pj.slice(1));
+ x2 = bez2[6];
+ y2 = bez2[7];
+ } else {
+ bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m];
+ x2 = x2m;
+ y2 = y2m;
+ }
+ var intr = interHelper(bez1, bez2, justCount);
+ if (justCount) {
+ res += intr;
+ } else {
+ for (var k = 0, kk = intr.length; k < kk; k++) {
+ intr[k].segment1 = i;
+ intr[k].segment2 = j;
+ intr[k].bez1 = bez1;
+ intr[k].bez2 = bez2;
+ }
+ res = res.concat(intr);
+ }
+ }
+ }
+ }
+ }
+ return res;
+ }
+ /*\
+ * Raphael.isPointInsidePath
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if given point is inside a given closed path.
+ > Parameters
+ - path (string) path string
+ - x (number) x of the point
+ - y (number) y of the point
+ = (boolean) true, if point is inside the path
+ \*/
+ R.isPointInsidePath = function (path, x, y) {
+ var bbox = R.pathBBox(path);
+ return R.isPointInsideBBox(bbox, x, y) &&
+ interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1;
+ };
+ R._removedFactory = function (methodname) {
+ return function () {
+ eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname);
+ };
+ };
+ /*\
+ * Raphael.pathBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Return bounding box of a given path
+ > Parameters
+ - path (string) path string
+ = (object) bounding box
+ o {
+ o x: (number) x coordinate of the left top point of the box
+ o y: (number) y coordinate of the left top point of the box
+ o x2: (number) x coordinate of the right bottom point of the box
+ o y2: (number) y coordinate of the right bottom point of the box
+ o width: (number) width of the box
+ o height: (number) height of the box
+ o cx: (number) x coordinate of the center of the box
+ o cy: (number) y coordinate of the center of the box
+ o }
+ \*/
+ var pathDimensions = R.pathBBox = function (path) {
+ var pth = paths(path);
+ if (pth.bbox) {
+ return clone(pth.bbox);
+ }
+ if (!path) {
+ return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0};
+ }
+ path = path2curve(path);
+ var x = 0,
+ y = 0,
+ X = [],
+ Y = [],
+ p;
+ for (var i = 0, ii = path.length; i < ii; i++) {
+ p = path[i];
+ if (p[0] == "M") {
+ x = p[1];
+ y = p[2];
+ X.push(x);
+ Y.push(y);
+ } else {
+ var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ X = X[concat](dim.min.x, dim.max.x);
+ Y = Y[concat](dim.min.y, dim.max.y);
+ x = p[5];
+ y = p[6];
+ }
+ }
+ var xmin = mmin[apply](0, X),
+ ymin = mmin[apply](0, Y),
+ xmax = mmax[apply](0, X),
+ ymax = mmax[apply](0, Y),
+ width = xmax - xmin,
+ height = ymax - ymin,
+ bb = {
+ x: xmin,
+ y: ymin,
+ x2: xmax,
+ y2: ymax,
+ width: width,
+ height: height,
+ cx: xmin + width / 2,
+ cy: ymin + height / 2
+ };
+ pth.bbox = clone(bb);
+ return bb;
+ },
+ pathClone = function (pathArray) {
+ var res = clone(pathArray);
+ res.toString = R._path2string;
+ return res;
+ },
+ pathToRelative = R._pathToRelative = function (pathArray) {
+ var pth = paths(pathArray);
+ if (pth.rel) {
+ return pathClone(pth.rel);
+ }
+ if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
+ pathArray = R.parsePathString(pathArray);
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ start = 0;
+ if (pathArray[0][0] == "M") {
+ x = pathArray[0][1];
+ y = pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res.push(["M", x, y]);
+ }
+ for (var i = start, ii = pathArray.length; i < ii; i++) {
+ var r = res[i] = [],
+ pa = pathArray[i];
+ if (pa[0] != lowerCase.call(pa[0])) {
+ r[0] = lowerCase.call(pa[0]);
+ switch (r[0]) {
+ case "a":
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] - x).toFixed(3);
+ r[7] = +(pa[7] - y).toFixed(3);
+ break;
+ case "v":
+ r[1] = +(pa[1] - y).toFixed(3);
+ break;
+ case "m":
+ mx = pa[1];
+ my = pa[2];
+ default:
+ for (var j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
+ }
+ }
+ } else {
+ r = res[i] = [];
+ if (pa[0] == "m") {
+ mx = pa[1] + x;
+ my = pa[2] + y;
+ }
+ for (var k = 0, kk = pa.length; k < kk; k++) {
+ res[i][k] = pa[k];
+ }
+ }
+ var len = res[i].length;
+ switch (res[i][0]) {
+ case "z":
+ x = mx;
+ y = my;
+ break;
+ case "h":
+ x += +res[i][len - 1];
+ break;
+ case "v":
+ y += +res[i][len - 1];
+ break;
+ default:
+ x += +res[i][len - 2];
+ y += +res[i][len - 1];
+ }
+ }
+ res.toString = R._path2string;
+ pth.rel = pathClone(res);
+ return res;
+ },
+ pathToAbsolute = R._pathToAbsolute = function (pathArray) {
+ var pth = paths(pathArray);
+ if (pth.abs) {
+ return pathClone(pth.abs);
+ }
+ if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
+ pathArray = R.parsePathString(pathArray);
+ }
+ if (!pathArray || !pathArray.length) {
+ return [["M", 0, 0]];
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ start = 0;
+ if (pathArray[0][0] == "M") {
+ x = +pathArray[0][1];
+ y = +pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res[0] = ["M", x, y];
+ }
+ var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z";
+ for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) {
+ res.push(r = []);
+ pa = pathArray[i];
+ if (pa[0] != upperCase.call(pa[0])) {
+ r[0] = upperCase.call(pa[0]);
+ switch (r[0]) {
+ case "A":
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] + x);
+ r[7] = +(pa[7] + y);
+ break;
+ case "V":
+ r[1] = +pa[1] + y;
+ break;
+ case "H":
+ r[1] = +pa[1] + x;
+ break;
+ case "R":
+ var dots = [x, y][concat](pa.slice(1));
+ for (var j = 2, jj = dots.length; j < jj; j++) {
+ dots[j] = +dots[j] + x;
+ dots[++j] = +dots[j] + y;
+ }
+ res.pop();
+ res = res[concat](catmullRom2bezier(dots, crz));
+ break;
+ case "M":
+ mx = +pa[1] + x;
+ my = +pa[2] + y;
+ default:
+ for (j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +pa[j] + ((j % 2) ? x : y);
+ }
+ }
+ } else if (pa[0] == "R") {
+ dots = [x, y][concat](pa.slice(1));
+ res.pop();
+ res = res[concat](catmullRom2bezier(dots, crz));
+ r = ["R"][concat](pa.slice(-2));
+ } else {
+ for (var k = 0, kk = pa.length; k < kk; k++) {
+ r[k] = pa[k];
+ }
+ }
+ switch (r[0]) {
+ case "Z":
+ x = mx;
+ y = my;
+ break;
+ case "H":
+ x = r[1];
+ break;
+ case "V":
+ y = r[1];
+ break;
+ case "M":
+ mx = r[r.length - 2];
+ my = r[r.length - 1];
+ default:
+ x = r[r.length - 2];
+ y = r[r.length - 1];
+ }
+ }
+ res.toString = R._path2string;
+ pth.abs = pathClone(res);
+ return res;
+ },
+ l2c = function (x1, y1, x2, y2) {
+ return [x1, y1, x2, y2, x2, y2];
+ },
+ q2c = function (x1, y1, ax, ay, x2, y2) {
+ var _13 = 1 / 3,
+ _23 = 2 / 3;
+ return [
+ _13 * x1 + _23 * ax,
+ _13 * y1 + _23 * ay,
+ _13 * x2 + _23 * ax,
+ _13 * y2 + _23 * ay,
+ x2,
+ y2
+ ];
+ },
+ a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
+ // for more information of where this math came from visit:
+ // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+ var _120 = PI * 120 / 180,
+ rad = PI / 180 * (+angle || 0),
+ res = [],
+ xy,
+ rotate = cacher(function (x, y, rad) {
+ var X = x * math.cos(rad) - y * math.sin(rad),
+ Y = x * math.sin(rad) + y * math.cos(rad);
+ return {x: X, y: Y};
+ });
+ if (!recursive) {
+ xy = rotate(x1, y1, -rad);
+ x1 = xy.x;
+ y1 = xy.y;
+ xy = rotate(x2, y2, -rad);
+ x2 = xy.x;
+ y2 = xy.y;
+ var cos = math.cos(PI / 180 * angle),
+ sin = math.sin(PI / 180 * angle),
+ x = (x1 - x2) / 2,
+ y = (y1 - y2) / 2;
+ var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
+ if (h > 1) {
+ h = math.sqrt(h);
+ rx = h * rx;
+ ry = h * ry;
+ }
+ var rx2 = rx * rx,
+ ry2 = ry * ry,
+ k = (large_arc_flag == sweep_flag ? -1 : 1) *
+ math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
+ cx = k * rx * y / ry + (x1 + x2) / 2,
+ cy = k * -ry * x / rx + (y1 + y2) / 2,
+ f1 = math.asin(((y1 - cy) / ry).toFixed(9)),
+ f2 = math.asin(((y2 - cy) / ry).toFixed(9));
+
+ f1 = x1 < cx ? PI - f1 : f1;
+ f2 = x2 < cx ? PI - f2 : f2;
+ f1 < 0 && (f1 = PI * 2 + f1);
+ f2 < 0 && (f2 = PI * 2 + f2);
+ if (sweep_flag && f1 > f2) {
+ f1 = f1 - PI * 2;
+ }
+ if (!sweep_flag && f2 > f1) {
+ f2 = f2 - PI * 2;
+ }
+ } else {
+ f1 = recursive[0];
+ f2 = recursive[1];
+ cx = recursive[2];
+ cy = recursive[3];
+ }
+ var df = f2 - f1;
+ if (abs(df) > _120) {
+ var f2old = f2,
+ x2old = x2,
+ y2old = y2;
+ f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
+ x2 = cx + rx * math.cos(f2);
+ y2 = cy + ry * math.sin(f2);
+ res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
+ }
+ df = f2 - f1;
+ var c1 = math.cos(f1),
+ s1 = math.sin(f1),
+ c2 = math.cos(f2),
+ s2 = math.sin(f2),
+ t = math.tan(df / 4),
+ hx = 4 / 3 * rx * t,
+ hy = 4 / 3 * ry * t,
+ m1 = [x1, y1],
+ m2 = [x1 + hx * s1, y1 - hy * c1],
+ m3 = [x2 + hx * s2, y2 - hy * c2],
+ m4 = [x2, y2];
+ m2[0] = 2 * m1[0] - m2[0];
+ m2[1] = 2 * m1[1] - m2[1];
+ if (recursive) {
+ return [m2, m3, m4][concat](res);
+ } else {
+ res = [m2, m3, m4][concat](res).join()[split](",");
+ var newres = [];
+ for (var i = 0, ii = res.length; i < ii; i++) {
+ newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
+ }
+ return newres;
+ }
+ },
+ findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+ var t1 = 1 - t;
+ return {
+ x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
+ y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y
+ };
+ },
+ curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+ var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
+ b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
+ c = p1x - c1x,
+ t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ y = [p1y, p2y],
+ x = [p1x, p2x],
+ dot;
+ abs(t1) > "1e12" && (t1 = .5);
+ abs(t2) > "1e12" && (t2 = .5);
+ if (t1 > 0 && t1 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
+ b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
+ c = p1y - c1y;
+ t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ abs(t1) > "1e12" && (t1 = .5);
+ abs(t2) > "1e12" && (t2 = .5);
+ if (t1 > 0 && t1 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ return {
+ min: {x: mmin[apply](0, x), y: mmin[apply](0, y)},
+ max: {x: mmax[apply](0, x), y: mmax[apply](0, y)}
+ };
+ }),
+ path2curve = R._path2curve = cacher(function (path, path2) {
+ var pth = !path2 && paths(path);
+ if (!path2 && pth.curve) {
+ return pathClone(pth.curve);
+ }
+ var p = pathToAbsolute(path),
+ p2 = path2 && pathToAbsolute(path2),
+ attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ processPath = function (path, d, pcom) {
+ var nx, ny, tq = {T:1, Q:1};
+ if (!path) {
+ return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
+ }
+ !(path[0] in tq) && (d.qx = d.qy = null);
+ switch (path[0]) {
+ case "M":
+ d.X = path[1];
+ d.Y = path[2];
+ break;
+ case "A":
+ path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1))));
+ break;
+ case "S":
+ if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S.
+ nx = d.x * 2 - d.bx; // And reflect the previous
+ ny = d.y * 2 - d.by; // command's control point relative to the current point.
+ }
+ else { // or some else or nothing
+ nx = d.x;
+ ny = d.y;
+ }
+ path = ["C", nx, ny][concat](path.slice(1));
+ break;
+ case "T":
+ if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T.
+ d.qx = d.x * 2 - d.qx; // And make a reflection similar
+ d.qy = d.y * 2 - d.qy; // to case "S".
+ }
+ else { // or something else or nothing
+ d.qx = d.x;
+ d.qy = d.y;
+ }
+ path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
+ break;
+ case "Q":
+ d.qx = path[1];
+ d.qy = path[2];
+ path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
+ break;
+ case "L":
+ path = ["C"][concat](l2c(d.x, d.y, path[1], path[2]));
+ break;
+ case "H":
+ path = ["C"][concat](l2c(d.x, d.y, path[1], d.y));
+ break;
+ case "V":
+ path = ["C"][concat](l2c(d.x, d.y, d.x, path[1]));
+ break;
+ case "Z":
+ path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y));
+ break;
+ }
+ return path;
+ },
+ fixArc = function (pp, i) {
+ if (pp[i].length > 7) {
+ pp[i].shift();
+ var pi = pp[i];
+ while (pi.length) {
+ pcoms1[i]="A"; // if created multiple C:s, their original seg is saved
+ p2 && (pcoms2[i]="A"); // the same as above
+ pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6)));
+ }
+ pp.splice(i, 1);
+ ii = mmax(p.length, p2 && p2.length || 0);
+ }
+ },
+ fixM = function (path1, path2, a1, a2, i) {
+ if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
+ path2.splice(i, 0, ["M", a2.x, a2.y]);
+ a1.bx = 0;
+ a1.by = 0;
+ a1.x = path1[i][1];
+ a1.y = path1[i][2];
+ ii = mmax(p.length, p2 && p2.length || 0);
+ }
+ },
+ pcoms1 = [], // path commands of original path p
+ pcoms2 = [], // path commands of original path p2
+ pfirst = "", // temporary holder for original path command
+ pcom = ""; // holder for previous path command of original path
+ for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) {
+ p[i] && (pfirst = p[i][0]); // save current path command
+
+ if (pfirst != "C") // C is not saved yet, because it may be result of conversion
+ {
+ pcoms1[i] = pfirst; // Save current path command
+ i && ( pcom = pcoms1[i-1]); // Get previous path command pcom
+ }
+ p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath
+
+ if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command
+ // which may produce multiple C:s
+ // so we have to make sure that C is also C in original path
+
+ fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1
+
+ if (p2) { // the same procedures is done to p2
+ p2[i] && (pfirst = p2[i][0]);
+ if (pfirst != "C")
+ {
+ pcoms2[i] = pfirst;
+ i && (pcom = pcoms2[i-1]);
+ }
+ p2[i] = processPath(p2[i], attrs2, pcom);
+
+ if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C";
+
+ fixArc(p2, i);
+ }
+ fixM(p, p2, attrs, attrs2, i);
+ fixM(p2, p, attrs2, attrs, i);
+ var seg = p[i],
+ seg2 = p2 && p2[i],
+ seglen = seg.length,
+ seg2len = p2 && seg2.length;
+ attrs.x = seg[seglen - 2];
+ attrs.y = seg[seglen - 1];
+ attrs.bx = toFloat(seg[seglen - 4]) || attrs.x;
+ attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
+ attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x);
+ attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y);
+ attrs2.x = p2 && seg2[seg2len - 2];
+ attrs2.y = p2 && seg2[seg2len - 1];
+ }
+ if (!p2) {
+ pth.curve = pathClone(p);
+ }
+ return p2 ? [p, p2] : p;
+ }, null, pathClone),
+ parseDots = R._parseDots = cacher(function (gradient) {
+ var dots = [];
+ for (var i = 0, ii = gradient.length; i < ii; i++) {
+ var dot = {},
+ par = gradient[i].match(/^([^:]*):?([\d\.]*)/);
+ dot.color = R.getRGB(par[1]);
+ if (dot.color.error) {
+ return null;
+ }
+ dot.color = dot.color.hex;
+ par[2] && (dot.offset = par[2] + "%");
+ dots.push(dot);
+ }
+ for (i = 1, ii = dots.length - 1; i < ii; i++) {
+ if (!dots[i].offset) {
+ var start = toFloat(dots[i - 1].offset || 0),
+ end = 0;
+ for (var j = i + 1; j < ii; j++) {
+ if (dots[j].offset) {
+ end = dots[j].offset;
+ break;
+ }
+ }
+ if (!end) {
+ end = 100;
+ j = ii;
+ }
+ end = toFloat(end);
+ var d = (end - start) / (j - i + 1);
+ for (; i < j; i++) {
+ start += d;
+ dots[i].offset = start + "%";
+ }
+ }
+ }
+ return dots;
+ }),
+ tear = R._tear = function (el, paper) {
+ el == paper.top && (paper.top = el.prev);
+ el == paper.bottom && (paper.bottom = el.next);
+ el.next && (el.next.prev = el.prev);
+ el.prev && (el.prev.next = el.next);
+ },
+ tofront = R._tofront = function (el, paper) {
+ if (paper.top === el) {
+ return;
+ }
+ tear(el, paper);
+ el.next = null;
+ el.prev = paper.top;
+ paper.top.next = el;
+ paper.top = el;
+ },
+ toback = R._toback = function (el, paper) {
+ if (paper.bottom === el) {
+ return;
+ }
+ tear(el, paper);
+ el.next = paper.bottom;
+ el.prev = null;
+ paper.bottom.prev = el;
+ paper.bottom = el;
+ },
+ insertafter = R._insertafter = function (el, el2, paper) {
+ tear(el, paper);
+ el2 == paper.top && (paper.top = el);
+ el2.next && (el2.next.prev = el);
+ el.next = el2.next;
+ el.prev = el2;
+ el2.next = el;
+ },
+ insertbefore = R._insertbefore = function (el, el2, paper) {
+ tear(el, paper);
+ el2 == paper.bottom && (paper.bottom = el);
+ el2.prev && (el2.prev.next = el);
+ el.prev = el2.prev;
+ el2.prev = el;
+ el.next = el2;
+ },
+ /*\
+ * Raphael.toMatrix
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns matrix of transformations applied to a given path
+ > Parameters
+ - path (string) path string
+ - transform (string|array) transformation string
+ = (object) @Matrix
+ \*/
+ toMatrix = R.toMatrix = function (path, transform) {
+ var bb = pathDimensions(path),
+ el = {
+ _: {
+ transform: E
+ },
+ getBBox: function () {
+ return bb;
+ }
+ };
+ extractTransform(el, transform);
+ return el.matrix;
+ },
+ /*\
+ * Raphael.transformPath
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns path transformed by a given transformation
+ > Parameters
+ - path (string) path string
+ - transform (string|array) transformation string
+ = (string) path
+ \*/
+ transformPath = R.transformPath = function (path, transform) {
+ return mapPath(path, toMatrix(path, transform));
+ },
+ extractTransform = R._extractTransform = function (el, tstr) {
+ if (tstr == null) {
+ return el._.transform;
+ }
+ tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E);
+ var tdata = R.parseTransformString(tstr),
+ deg = 0,
+ dx = 0,
+ dy = 0,
+ sx = 1,
+ sy = 1,
+ _ = el._,
+ m = new Matrix;
+ _.transform = tdata || [];
+ if (tdata) {
+ for (var i = 0, ii = tdata.length; i < ii; i++) {
+ var t = tdata[i],
+ tlen = t.length,
+ command = Str(t[0]).toLowerCase(),
+ absolute = t[0] != command,
+ inver = absolute ? m.invert() : 0,
+ x1,
+ y1,
+ x2,
+ y2,
+ bb;
+ if (command == "t" && tlen == 3) {
+ if (absolute) {
+ x1 = inver.x(0, 0);
+ y1 = inver.y(0, 0);
+ x2 = inver.x(t[1], t[2]);
+ y2 = inver.y(t[1], t[2]);
+ m.translate(x2 - x1, y2 - y1);
+ } else {
+ m.translate(t[1], t[2]);
+ }
+ } else if (command == "r") {
+ if (tlen == 2) {
+ bb = bb || el.getBBox(1);
+ m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2);
+ deg += t[1];
+ } else if (tlen == 4) {
+ if (absolute) {
+ x2 = inver.x(t[2], t[3]);
+ y2 = inver.y(t[2], t[3]);
+ m.rotate(t[1], x2, y2);
+ } else {
+ m.rotate(t[1], t[2], t[3]);
+ }
+ deg += t[1];
+ }
+ } else if (command == "s") {
+ if (tlen == 2 || tlen == 3) {
+ bb = bb || el.getBBox(1);
+ m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2);
+ sx *= t[1];
+ sy *= t[tlen - 1];
+ } else if (tlen == 5) {
+ if (absolute) {
+ x2 = inver.x(t[3], t[4]);
+ y2 = inver.y(t[3], t[4]);
+ m.scale(t[1], t[2], x2, y2);
+ } else {
+ m.scale(t[1], t[2], t[3], t[4]);
+ }
+ sx *= t[1];
+ sy *= t[2];
+ }
+ } else if (command == "m" && tlen == 7) {
+ m.add(t[1], t[2], t[3], t[4], t[5], t[6]);
+ }
+ _.dirtyT = 1;
+ el.matrix = m;
+ }
+ }
+
+ /*\
+ * Element.matrix
+ [ property (object) ]
+ **
+ * Keeps @Matrix object, which represents element transformation
+ \*/
+ el.matrix = m;
+
+ _.sx = sx;
+ _.sy = sy;
+ _.deg = deg;
+ _.dx = dx = m.e;
+ _.dy = dy = m.f;
+
+ if (sx == 1 && sy == 1 && !deg && _.bbox) {
+ _.bbox.x += +dx;
+ _.bbox.y += +dy;
+ } else {
+ _.dirtyT = 1;
+ }
+ },
+ getEmpty = function (item) {
+ var l = item[0];
+ switch (l.toLowerCase()) {
+ case "t": return [l, 0, 0];
+ case "m": return [l, 1, 0, 0, 1, 0, 0];
+ case "r": if (item.length == 4) {
+ return [l, 0, item[2], item[3]];
+ } else {
+ return [l, 0];
+ }
+ case "s": if (item.length == 5) {
+ return [l, 1, 1, item[3], item[4]];
+ } else if (item.length == 3) {
+ return [l, 1, 1];
+ } else {
+ return [l, 1];
+ }
+ }
+ },
+ equaliseTransform = R._equaliseTransform = function (t1, t2) {
+ t2 = Str(t2).replace(/\.{3}|\u2026/g, t1);
+ t1 = R.parseTransformString(t1) || [];
+ t2 = R.parseTransformString(t2) || [];
+ var maxlength = mmax(t1.length, t2.length),
+ from = [],
+ to = [],
+ i = 0, j, jj,
+ tt1, tt2;
+ for (; i < maxlength; i++) {
+ tt1 = t1[i] || getEmpty(t2[i]);
+ tt2 = t2[i] || getEmpty(tt1);
+ if ((tt1[0] != tt2[0]) ||
+ (tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) ||
+ (tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4]))
+ ) {
+ return;
+ }
+ from[i] = [];
+ to[i] = [];
+ for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) {
+ j in tt1 && (from[i][j] = tt1[j]);
+ j in tt2 && (to[i][j] = tt2[j]);
+ }
+ }
+ return {
+ from: from,
+ to: to
+ };
+ };
+ R._getContainer = function (x, y, w, h) {
+ var container;
+ container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x;
+ if (container == null) {
+ return;
+ }
+ if (container.tagName) {
+ if (y == null) {
+ return {
+ container: container,
+ width: container.style.pixelWidth || container.offsetWidth,
+ height: container.style.pixelHeight || container.offsetHeight
+ };
+ } else {
+ return {
+ container: container,
+ width: y,
+ height: w
+ };
+ }
+ }
+ return {
+ container: 1,
+ x: x,
+ y: y,
+ width: w,
+ height: h
+ };
+ };
+ /*\
+ * Raphael.pathToRelative
+ [ method ]
+ **
+ * Utility method
+ **
+ * Converts path to relative form
+ > Parameters
+ - pathString (string|array) path string or array of segments
+ = (array) array of segments.
+ \*/
+ R.pathToRelative = pathToRelative;
+ R._engine = {};
+ /*\
+ * Raphael.path2curve
+ [ method ]
+ **
+ * Utility method
+ **
+ * Converts path to a new path where all segments are cubic bezier curves.
+ > Parameters
+ - pathString (string|array) path string or array of segments
+ = (array) array of segments.
+ \*/
+ R.path2curve = path2curve;
+ /*\
+ * Raphael.matrix
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns matrix based on given parameters.
+ > Parameters
+ - a (number)
+ - b (number)
+ - c (number)
+ - d (number)
+ - e (number)
+ - f (number)
+ = (object) @Matrix
+ \*/
+ R.matrix = function (a, b, c, d, e, f) {
+ return new Matrix(a, b, c, d, e, f);
+ };
+ function Matrix(a, b, c, d, e, f) {
+ if (a != null) {
+ this.a = +a;
+ this.b = +b;
+ this.c = +c;
+ this.d = +d;
+ this.e = +e;
+ this.f = +f;
+ } else {
+ this.a = 1;
+ this.b = 0;
+ this.c = 0;
+ this.d = 1;
+ this.e = 0;
+ this.f = 0;
+ }
+ }
+ (function (matrixproto) {
+ /*\
+ * Matrix.add
+ [ method ]
+ **
+ * Adds given matrix to existing one.
+ > Parameters
+ - a (number)
+ - b (number)
+ - c (number)
+ - d (number)
+ - e (number)
+ - f (number)
+ or
+ - matrix (object) @Matrix
+ \*/
+ matrixproto.add = function (a, b, c, d, e, f) {
+ var out = [[], [], []],
+ m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]],
+ matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
+ x, y, z, res;
+
+ if (a && a instanceof Matrix) {
+ matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]];
+ }
+
+ for (x = 0; x < 3; x++) {
+ for (y = 0; y < 3; y++) {
+ res = 0;
+ for (z = 0; z < 3; z++) {
+ res += m[x][z] * matrix[z][y];
+ }
+ out[x][y] = res;
+ }
+ }
+ this.a = out[0][0];
+ this.b = out[1][0];
+ this.c = out[0][1];
+ this.d = out[1][1];
+ this.e = out[0][2];
+ this.f = out[1][2];
+ };
+ /*\
+ * Matrix.invert
+ [ method ]
+ **
+ * Returns inverted version of the matrix
+ = (object) @Matrix
+ \*/
+ matrixproto.invert = function () {
+ var me = this,
+ x = me.a * me.d - me.b * me.c;
+ return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x);
+ };
+ /*\
+ * Matrix.clone
+ [ method ]
+ **
+ * Returns copy of the matrix
+ = (object) @Matrix
+ \*/
+ matrixproto.clone = function () {
+ return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f);
+ };
+ /*\
+ * Matrix.translate
+ [ method ]
+ **
+ * Translate the matrix
+ > Parameters
+ - x (number)
+ - y (number)
+ \*/
+ matrixproto.translate = function (x, y) {
+ this.add(1, 0, 0, 1, x, y);
+ };
+ /*\
+ * Matrix.scale
+ [ method ]
+ **
+ * Scales the matrix
+ > Parameters
+ - x (number)
+ - y (number) #optional
+ - cx (number) #optional
+ - cy (number) #optional
+ \*/
+ matrixproto.scale = function (x, y, cx, cy) {
+ y == null && (y = x);
+ (cx || cy) && this.add(1, 0, 0, 1, cx, cy);
+ this.add(x, 0, 0, y, 0, 0);
+ (cx || cy) && this.add(1, 0, 0, 1, -cx, -cy);
+ };
+ /*\
+ * Matrix.rotate
+ [ method ]
+ **
+ * Rotates the matrix
+ > Parameters
+ - a (number)
+ - x (number)
+ - y (number)
+ \*/
+ matrixproto.rotate = function (a, x, y) {
+ a = R.rad(a);
+ x = x || 0;
+ y = y || 0;
+ var cos = +math.cos(a).toFixed(9),
+ sin = +math.sin(a).toFixed(9);
+ this.add(cos, sin, -sin, cos, x, y);
+ this.add(1, 0, 0, 1, -x, -y);
+ };
+ /*\
+ * Matrix.x
+ [ method ]
+ **
+ * Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y
+ > Parameters
+ - x (number)
+ - y (number)
+ = (number) x
+ \*/
+ matrixproto.x = function (x, y) {
+ return x * this.a + y * this.c + this.e;
+ };
+ /*\
+ * Matrix.y
+ [ method ]
+ **
+ * Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x
+ > Parameters
+ - x (number)
+ - y (number)
+ = (number) y
+ \*/
+ matrixproto.y = function (x, y) {
+ return x * this.b + y * this.d + this.f;
+ };
+ matrixproto.get = function (i) {
+ return +this[Str.fromCharCode(97 + i)].toFixed(4);
+ };
+ matrixproto.toString = function () {
+ return R.svg ?
+ "matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" :
+ [this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join();
+ };
+ matrixproto.toFilter = function () {
+ return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) +
+ ", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) +
+ ", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')";
+ };
+ matrixproto.offset = function () {
+ return [this.e.toFixed(4), this.f.toFixed(4)];
+ };
+ function norm(a) {
+ return a[0] * a[0] + a[1] * a[1];
+ }
+ function normalize(a) {
+ var mag = math.sqrt(norm(a));
+ a[0] && (a[0] /= mag);
+ a[1] && (a[1] /= mag);
+ }
+ /*\
+ * Matrix.split
+ [ method ]
+ **
+ * Splits matrix into primitive transformations
+ = (object) in format:
+ o dx (number) translation by x
+ o dy (number) translation by y
+ o scalex (number) scale by x
+ o scaley (number) scale by y
+ o shear (number) shear
+ o rotate (number) rotation in deg
+ o isSimple (boolean) could it be represented via simple transformations
+ \*/
+ matrixproto.split = function () {
+ var out = {};
+ // translation
+ out.dx = this.e;
+ out.dy = this.f;
+
+ // scale and shear
+ var row = [[this.a, this.c], [this.b, this.d]];
+ out.scalex = math.sqrt(norm(row[0]));
+ normalize(row[0]);
+
+ out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
+ row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
+
+ out.scaley = math.sqrt(norm(row[1]));
+ normalize(row[1]);
+ out.shear /= out.scaley;
+
+ // rotation
+ var sin = -row[0][1],
+ cos = row[1][1];
+ if (cos < 0) {
+ out.rotate = R.deg(math.acos(cos));
+ if (sin < 0) {
+ out.rotate = 360 - out.rotate;
+ }
+ } else {
+ out.rotate = R.deg(math.asin(sin));
+ }
+
+ out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate);
+ out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate;
+ out.noRotation = !+out.shear.toFixed(9) && !out.rotate;
+ return out;
+ };
+ /*\
+ * Matrix.toTransformString
+ [ method ]
+ **
+ * Return transform string that represents given matrix
+ = (string) transform string
+ \*/
+ matrixproto.toTransformString = function (shorter) {
+ var s = shorter || this[split]();
+ if (s.isSimple) {
+ s.scalex = +s.scalex.toFixed(4);
+ s.scaley = +s.scaley.toFixed(4);
+ s.rotate = +s.rotate.toFixed(4);
+ return (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) +
+ (s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) +
+ (s.rotate ? "r" + [s.rotate, 0, 0] : E);
+ } else {
+ return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)];
+ }
+ };
+ })(Matrix.prototype);
+
+ // WebKit rendering bug workaround method
+ var version = navigator.userAgent.match(/Version\/(.*?)\s/) || navigator.userAgent.match(/Chrome\/(\d+)/);
+ if ((navigator.vendor == "Apple Computer, Inc.") && (version && version[1] < 4 || navigator.platform.slice(0, 2) == "iP") ||
+ (navigator.vendor == "Google Inc." && version && version[1] < 8)) {
+ /*\
+ * Paper.safari
+ [ method ]
+ **
+ * There is an inconvenient rendering bug in Safari (WebKit):
+ * sometimes the rendering should be forced.
+ * This method should help with dealing with this bug.
+ \*/
+ paperproto.safari = function () {
+ var rect = this.rect(-99, -99, this.width + 99, this.height + 99).attr({stroke: "none"});
+ setTimeout(function () {rect.remove();});
+ };
+ } else {
+ paperproto.safari = fun;
+ }
+
+ var preventDefault = function () {
+ this.returnValue = false;
+ },
+ preventTouch = function () {
+ return this.originalEvent.preventDefault();
+ },
+ stopPropagation = function () {
+ this.cancelBubble = true;
+ },
+ stopTouch = function () {
+ return this.originalEvent.stopPropagation();
+ },
+ getEventPosition = function (e) {
+ var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
+
+ return {
+ x: e.clientX + scrollX,
+ y: e.clientY + scrollY
+ };
+ },
+ addEvent = (function () {
+ if (g.doc.addEventListener) {
+ return function (obj, type, fn, element) {
+ var f = function (e) {
+ var pos = getEventPosition(e);
+ return fn.call(element, e, pos.x, pos.y);
+ };
+ obj.addEventListener(type, f, false);
+
+ if (supportsTouch && touchMap[type]) {
+ var _f = function (e) {
+ var pos = getEventPosition(e),
+ olde = e;
+
+ for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) {
+ if (e.targetTouches[i].target == obj) {
+ e = e.targetTouches[i];
+ e.originalEvent = olde;
+ e.preventDefault = preventTouch;
+ e.stopPropagation = stopTouch;
+ break;
+ }
+ }
+
+ return fn.call(element, e, pos.x, pos.y);
+ };
+ obj.addEventListener(touchMap[type], _f, false);
+ }
+
+ return function () {
+ obj.removeEventListener(type, f, false);
+
+ if (supportsTouch && touchMap[type])
+ obj.removeEventListener(touchMap[type], _f, false);
+
+ return true;
+ };
+ };
+ } else if (g.doc.attachEvent) {
+ return function (obj, type, fn, element) {
+ var f = function (e) {
+ e = e || g.win.event;
+ var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
+ x = e.clientX + scrollX,
+ y = e.clientY + scrollY;
+ e.preventDefault = e.preventDefault || preventDefault;
+ e.stopPropagation = e.stopPropagation || stopPropagation;
+ return fn.call(element, e, x, y);
+ };
+ obj.attachEvent("on" + type, f);
+ var detacher = function () {
+ obj.detachEvent("on" + type, f);
+ return true;
+ };
+ return detacher;
+ };
+ }
+ })(),
+ drag = [],
+ dragMove = function (e) {
+ var x = e.clientX,
+ y = e.clientY,
+ scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
+ dragi,
+ j = drag.length;
+ while (j--) {
+ dragi = drag[j];
+ if (supportsTouch && e.touches) {
+ var i = e.touches.length,
+ touch;
+ while (i--) {
+ touch = e.touches[i];
+ if (touch.identifier == dragi.el._drag.id) {
+ x = touch.clientX;
+ y = touch.clientY;
+ (e.originalEvent ? e.originalEvent : e).preventDefault();
+ break;
+ }
+ }
+ } else {
+ e.preventDefault();
+ }
+ var node = dragi.el.node,
+ o,
+ next = node.nextSibling,
+ parent = node.parentNode,
+ display = node.style.display;
+ g.win.opera && parent.removeChild(node);
+ node.style.display = "none";
+ o = dragi.el.paper.getElementByPoint(x, y);
+ node.style.display = display;
+ g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node));
+ o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o);
+ x += scrollX;
+ y += scrollY;
+ eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e);
+ }
+ },
+ dragUp = function (e) {
+ R.unmousemove(dragMove).unmouseup(dragUp);
+ var i = drag.length,
+ dragi;
+ while (i--) {
+ dragi = drag[i];
+ dragi.el._drag = {};
+ eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e);
+ }
+ drag = [];
+ },
+ /*\
+ * Raphael.el
+ [ property (object) ]
+ **
+ * You can add your own method to elements. This is usefull when you want to hack default functionality or
+ * want to wrap some common transformation or attributes in one method. In difference to canvas methods,
+ * you can redefine element method at any time. Expending element methods wouldn’t affect set.
+ > Usage
+ | Raphael.el.red = function () {
+ | this.attr({fill: "#f00"});
+ | };
+ | // then use it
+ | paper.circle(100, 100, 20).red();
+ \*/
+ elproto = R.el = {};
+ /*\
+ * Element.click
+ [ method ]
+ **
+ * Adds event handler for click for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unclick
+ [ method ]
+ **
+ * Removes event handler for click for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.dblclick
+ [ method ]
+ **
+ * Adds event handler for double click for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.undblclick
+ [ method ]
+ **
+ * Removes event handler for double click for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mousedown
+ [ method ]
+ **
+ * Adds event handler for mousedown for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmousedown
+ [ method ]
+ **
+ * Removes event handler for mousedown for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mousemove
+ [ method ]
+ **
+ * Adds event handler for mousemove for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmousemove
+ [ method ]
+ **
+ * Removes event handler for mousemove for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseout
+ [ method ]
+ **
+ * Adds event handler for mouseout for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseout
+ [ method ]
+ **
+ * Removes event handler for mouseout for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseover
+ [ method ]
+ **
+ * Adds event handler for mouseover for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseover
+ [ method ]
+ **
+ * Removes event handler for mouseover for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseup
+ [ method ]
+ **
+ * Adds event handler for mouseup for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseup
+ [ method ]
+ **
+ * Removes event handler for mouseup for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchstart
+ [ method ]
+ **
+ * Adds event handler for touchstart for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchstart
+ [ method ]
+ **
+ * Removes event handler for touchstart for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchmove
+ [ method ]
+ **
+ * Adds event handler for touchmove for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchmove
+ [ method ]
+ **
+ * Removes event handler for touchmove for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchend
+ [ method ]
+ **
+ * Adds event handler for touchend for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchend
+ [ method ]
+ **
+ * Removes event handler for touchend for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchcancel
+ [ method ]
+ **
+ * Adds event handler for touchcancel for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchcancel
+ [ method ]
+ **
+ * Removes event handler for touchcancel for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+ for (var i = events.length; i--;) {
+ (function (eventName) {
+ R[eventName] = elproto[eventName] = function (fn, scope) {
+ if (R.is(fn, "function")) {
+ this.events = this.events || [];
+ this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)});
+ }
+ return this;
+ };
+ R["un" + eventName] = elproto["un" + eventName] = function (fn) {
+ var events = this.events || [],
+ l = events.length;
+ while (l--){
+ if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) {
+ events[l].unbind();
+ events.splice(l, 1);
+ !events.length && delete this.events;
+ }
+ }
+ return this;
+ };
+ })(events[i]);
+ }
+
+ /*\
+ * Element.data
+ [ method ]
+ **
+ * Adds or retrieves given value asociated with given key.
+ **
+ * See also @Element.removeData
+ > Parameters
+ - key (string) key to store data
+ - value (any) #optional value to store
+ = (object) @Element
+ * or, if value is not specified:
+ = (any) value
+ * or, if key and value are not specified:
+ = (object) Key/value pairs for all the data associated with the element.
+ > Usage
+ | for (var i = 0, i < 5, i++) {
+ | paper.circle(10 + 15 * i, 10, 10)
+ | .attr({fill: "#000"})
+ | .data("i", i)
+ | .click(function () {
+ | alert(this.data("i"));
+ | });
+ | }
+ \*/
+ elproto.data = function (key, value) {
+ var data = eldata[this.id] = eldata[this.id] || {};
+ if (arguments.length == 0) {
+ return data;
+ }
+ if (arguments.length == 1) {
+ if (R.is(key, "object")) {
+ for (var i in key) if (key[has](i)) {
+ this.data(i, key[i]);
+ }
+ return this;
+ }
+ eve("raphael.data.get." + this.id, this, data[key], key);
+ return data[key];
+ }
+ data[key] = value;
+ eve("raphael.data.set." + this.id, this, value, key);
+ return this;
+ };
+ /*\
+ * Element.removeData
+ [ method ]
+ **
+ * Removes value associated with an element by given key.
+ * If key is not provided, removes all the data of the element.
+ > Parameters
+ - key (string) #optional key
+ = (object) @Element
+ \*/
+ elproto.removeData = function (key) {
+ if (key == null) {
+ eldata[this.id] = {};
+ } else {
+ eldata[this.id] && delete eldata[this.id][key];
+ }
+ return this;
+ };
+ /*\
+ * Element.getData
+ [ method ]
+ **
+ * Retrieves the element data
+ = (object) data
+ \*/
+ elproto.getData = function () {
+ return clone(eldata[this.id] || {});
+ };
+ /*\
+ * Element.hover
+ [ method ]
+ **
+ * Adds event handlers for hover for the element.
+ > Parameters
+ - f_in (function) handler for hover in
+ - f_out (function) handler for hover out
+ - icontext (object) #optional context for hover in handler
+ - ocontext (object) #optional context for hover out handler
+ = (object) @Element
+ \*/
+ elproto.hover = function (f_in, f_out, scope_in, scope_out) {
+ return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in);
+ };
+ /*\
+ * Element.unhover
+ [ method ]
+ **
+ * Removes event handlers for hover for the element.
+ > Parameters
+ - f_in (function) handler for hover in
+ - f_out (function) handler for hover out
+ = (object) @Element
+ \*/
+ elproto.unhover = function (f_in, f_out) {
+ return this.unmouseover(f_in).unmouseout(f_out);
+ };
+ var draggable = [];
+ /*\
+ * Element.drag
+ [ method ]
+ **
+ * Adds event handlers for drag of the element.
+ > Parameters
+ - onmove (function) handler for moving
+ - onstart (function) handler for drag start
+ - onend (function) handler for drag end
+ - mcontext (object) #optional context for moving handler
+ - scontext (object) #optional context for drag start handler
+ - econtext (object) #optional context for drag end handler
+ * Additionaly following `drag` events will be triggered: `drag.start.<id>` on start,
+ * `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element
+ * `drag.over.<id>` will be fired as well.
+ *
+ * Start event and start handler will be called in specified context or in context of the element with following parameters:
+ o x (number) x position of the mouse
+ o y (number) y position of the mouse
+ o event (object) DOM event object
+ * Move event and move handler will be called in specified context or in context of the element with following parameters:
+ o dx (number) shift by x from the start point
+ o dy (number) shift by y from the start point
+ o x (number) x position of the mouse
+ o y (number) y position of the mouse
+ o event (object) DOM event object
+ * End event and end handler will be called in specified context or in context of the element with following parameters:
+ o event (object) DOM event object
+ = (object) @Element
+ \*/
+ elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) {
+ function start(e) {
+ (e.originalEvent || e).preventDefault();
+ var x = e.clientX,
+ y = e.clientY,
+ scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
+ this._drag.id = e.identifier;
+ if (supportsTouch && e.touches) {
+ var i = e.touches.length, touch;
+ while (i--) {
+ touch = e.touches[i];
+ this._drag.id = touch.identifier;
+ if (touch.identifier == this._drag.id) {
+ x = touch.clientX;
+ y = touch.clientY;
+ break;
+ }
+ }
+ }
+ this._drag.x = x + scrollX;
+ this._drag.y = y + scrollY;
+ !drag.length && R.mousemove(dragMove).mouseup(dragUp);
+ drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope});
+ onstart && eve.on("raphael.drag.start." + this.id, onstart);
+ onmove && eve.on("raphael.drag.move." + this.id, onmove);
+ onend && eve.on("raphael.drag.end." + this.id, onend);
+ eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e);
+ }
+ this._drag = {};
+ draggable.push({el: this, start: start});
+ this.mousedown(start);
+ return this;
+ };
+ /*\
+ * Element.onDragOver
+ [ method ]
+ **
+ * Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id).
+ > Parameters
+ - f (function) handler for event, first argument would be the element you are dragging over
+ \*/
+ elproto.onDragOver = function (f) {
+ f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id);
+ };
+ /*\
+ * Element.undrag
+ [ method ]
+ **
+ * Removes all drag event handlers from given element.
+ \*/
+ elproto.undrag = function () {
+ var i = draggable.length;
+ while (i--) if (draggable[i].el == this) {
+ this.unmousedown(draggable[i].start);
+ draggable.splice(i, 1);
+ eve.unbind("raphael.drag.*." + this.id);
+ }
+ !draggable.length && R.unmousemove(dragMove).unmouseup(dragUp);
+ drag = [];
+ };
+ /*\
+ * Paper.circle
+ [ method ]
+ **
+ * Draws a circle.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the centre
+ - y (number) y coordinate of the centre
+ - r (number) radius
+ = (object) Raphaël element object with type “circle”
+ **
+ > Usage
+ | var c = paper.circle(50, 50, 40);
+ \*/
+ paperproto.circle = function (x, y, r) {
+ var out = R._engine.circle(this, x || 0, y || 0, r || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.rect
+ [ method ]
+ *
+ * Draws a rectangle.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the top left corner
+ - y (number) y coordinate of the top left corner
+ - width (number) width
+ - height (number) height
+ - r (number) #optional radius for rounded corners, default is 0
+ = (object) Raphaël element object with type “rect”
+ **
+ > Usage
+ | // regular rectangle
+ | var c = paper.rect(10, 10, 50, 50);
+ | // rectangle with rounded corners
+ | var c = paper.rect(40, 40, 50, 50, 10);
+ \*/
+ paperproto.rect = function (x, y, w, h, r) {
+ var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.ellipse
+ [ method ]
+ **
+ * Draws an ellipse.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the centre
+ - y (number) y coordinate of the centre
+ - rx (number) horizontal radius
+ - ry (number) vertical radius
+ = (object) Raphaël element object with type “ellipse”
+ **
+ > Usage
+ | var c = paper.ellipse(50, 50, 40, 20);
+ \*/
+ paperproto.ellipse = function (x, y, rx, ry) {
+ var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.path
+ [ method ]
+ **
+ * Creates a path element by given path data string.
+ > Parameters
+ - pathString (string) #optional path string in SVG format.
+ * Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example:
+ | "M10,20L30,40"
+ * Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative.
+ *
+ # <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p>
+ # <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody>
+ # <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr>
+ # <tr><td>Z</td><td>closepath</td><td>(none)</td></tr>
+ # <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr>
+ # <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr>
+ # <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr>
+ # <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr>
+ # <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr>
+ # <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr>
+ # <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr>
+ # <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr>
+ # <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table>
+ * * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier.
+ * Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning.
+ > Usage
+ | var c = paper.path("M10 10L90 90");
+ | // draw a diagonal line:
+ | // move to 10,10, line to 90,90
+ * For example of path strings, check out these icons: http://raphaeljs.com/icons/
+ \*/
+ paperproto.path = function (pathString) {
+ pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E);
+ var out = R._engine.path(R.format[apply](R, arguments), this);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.image
+ [ method ]
+ **
+ * Embeds an image into the surface.
+ **
+ > Parameters
+ **
+ - src (string) URI of the source image
+ - x (number) x coordinate position
+ - y (number) y coordinate position
+ - width (number) width of the image
+ - height (number) height of the image
+ = (object) Raphaël element object with type “image”
+ **
+ > Usage
+ | var c = paper.image("apple.png", 10, 10, 80, 80);
+ \*/
+ paperproto.image = function (src, x, y, w, h) {
+ var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.text
+ [ method ]
+ **
+ * Draws a text string. If you need line breaks, put “\n” in the string.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate position
+ - y (number) y coordinate position
+ - text (string) The text string to draw
+ = (object) Raphaël element object with type “text”
+ **
+ > Usage
+ | var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!");
+ \*/
+ paperproto.text = function (x, y, text) {
+ var out = R._engine.text(this, x || 0, y || 0, Str(text));
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.set
+ [ method ]
+ **
+ * Creates array-like object to keep and operate several elements at once.
+ * Warning: it doesn’t create any elements for itself in the page, it just groups existing elements.
+ * Sets act as pseudo elements — all methods available to an element can be used on a set.
+ = (object) array-like object that represents set of elements
+ **
+ > Usage
+ | var st = paper.set();
+ | st.push(
+ | paper.circle(10, 10, 5),
+ | paper.circle(30, 10, 5)
+ | );
+ | st.attr({fill: "red"}); // changes the fill of both circles
+ \*/
+ paperproto.set = function (itemsArray) {
+ !R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length));
+ var out = new Set(itemsArray);
+ this.__set__ && this.__set__.push(out);
+ out["paper"] = this;
+ out["type"] = "set";
+ return out;
+ };
+ /*\
+ * Paper.setStart
+ [ method ]
+ **
+ * Creates @Paper.set. All elements that will be created after calling this method and before calling
+ * @Paper.setFinish will be added to the set.
+ **
+ > Usage
+ | paper.setStart();
+ | paper.circle(10, 10, 5),
+ | paper.circle(30, 10, 5)
+ | var st = paper.setFinish();
+ | st.attr({fill: "red"}); // changes the fill of both circles
+ \*/
+ paperproto.setStart = function (set) {
+ this.__set__ = set || this.set();
+ };
+ /*\
+ * Paper.setFinish
+ [ method ]
+ **
+ * See @Paper.setStart. This method finishes catching and returns resulting set.
+ **
+ = (object) set
+ \*/
+ paperproto.setFinish = function (set) {
+ var out = this.__set__;
+ delete this.__set__;
+ return out;
+ };
+ /*\
+ * Paper.getSize
+ [ method ]
+ **
+ * Obtains current paper actual size.
+ **
+ = (object)
+ \*/
+ paperproto.getSize = function () {
+ var container = this.canvas.parentNode;
+ return {
+ width: container.offsetWidth,
+ height: container.offsetHeight
+ };
+ };
+ /*\
+ * Paper.setSize
+ [ method ]
+ **
+ * If you need to change dimensions of the canvas call this method
+ **
+ > Parameters
+ **
+ - width (number) new width of the canvas
+ - height (number) new height of the canvas
+ \*/
+ paperproto.setSize = function (width, height) {
+ return R._engine.setSize.call(this, width, height);
+ };
+ /*\
+ * Paper.setViewBox
+ [ method ]
+ **
+ * Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by
+ * specifying new boundaries.
+ **
+ > Parameters
+ **
+ - x (number) new x position, default is `0`
+ - y (number) new y position, default is `0`
+ - w (number) new width of the canvas
+ - h (number) new height of the canvas
+ - fit (boolean) `true` if you want graphics to fit into new boundary box
+ \*/
+ paperproto.setViewBox = function (x, y, w, h, fit) {
+ return R._engine.setViewBox.call(this, x, y, w, h, fit);
+ };
+ /*\
+ * Paper.top
+ [ property ]
+ **
+ * Points to the topmost element on the paper
+ \*/
+ /*\
+ * Paper.bottom
+ [ property ]
+ **
+ * Points to the bottom element on the paper
+ \*/
+ paperproto.top = paperproto.bottom = null;
+ /*\
+ * Paper.raphael
+ [ property ]
+ **
+ * Points to the @Raphael object/function
+ \*/
+ paperproto.raphael = R;
+ var getOffset = function (elem) {
+ var box = elem.getBoundingClientRect(),
+ doc = elem.ownerDocument,
+ body = doc.body,
+ docElem = doc.documentElement,
+ clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop,
+ left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft;
+ return {
+ y: top,
+ x: left
+ };
+ };
+ /*\
+ * Paper.getElementByPoint
+ [ method ]
+ **
+ * Returns you topmost element under given point.
+ **
+ = (object) Raphaël element object
+ > Parameters
+ **
+ - x (number) x coordinate from the top left corner of the window
+ - y (number) y coordinate from the top left corner of the window
+ > Usage
+ | paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"});
+ \*/
+ paperproto.getElementByPoint = function (x, y) {
+ var paper = this,
+ svg = paper.canvas,
+ target = g.doc.elementFromPoint(x, y);
+ if (g.win.opera && target.tagName == "svg") {
+ var so = getOffset(svg),
+ sr = svg.createSVGRect();
+ sr.x = x - so.x;
+ sr.y = y - so.y;
+ sr.width = sr.height = 1;
+ var hits = svg.getIntersectionList(sr, null);
+ if (hits.length) {
+ target = hits[hits.length - 1];
+ }
+ }
+ if (!target) {
+ return null;
+ }
+ while (target.parentNode && target != svg.parentNode && !target.raphael) {
+ target = target.parentNode;
+ }
+ target == paper.canvas.parentNode && (target = svg);
+ target = target && target.raphael ? paper.getById(target.raphaelid) : null;
+ return target;
+ };
+
+ /*\
+ * Paper.getElementsByBBox
+ [ method ]
+ **
+ * Returns set of elements that have an intersecting bounding box
+ **
+ > Parameters
+ **
+ - bbox (object) bbox to check with
+ = (object) @Set
+ \*/
+ paperproto.getElementsByBBox = function (bbox) {
+ var set = this.set();
+ this.forEach(function (el) {
+ if (R.isBBoxIntersect(el.getBBox(), bbox)) {
+ set.push(el);
+ }
+ });
+ return set;
+ };
+
+ /*\
+ * Paper.getById
+ [ method ]
+ **
+ * Returns you element by its internal ID.
+ **
+ > Parameters
+ **
+ - id (number) id
+ = (object) Raphaël element object
+ \*/
+ paperproto.getById = function (id) {
+ var bot = this.bottom;
+ while (bot) {
+ if (bot.id == id) {
+ return bot;
+ }
+ bot = bot.next;
+ }
+ return null;
+ };
+ /*\
+ * Paper.forEach
+ [ method ]
+ **
+ * Executes given function for each element on the paper
+ *
+ * If callback function returns `false` it will stop loop running.
+ **
+ > Parameters
+ **
+ - callback (function) function to run
+ - thisArg (object) context object for the callback
+ = (object) Paper object
+ > Usage
+ | paper.forEach(function (el) {
+ | el.attr({ stroke: "blue" });
+ | });
+ \*/
+ paperproto.forEach = function (callback, thisArg) {
+ var bot = this.bottom;
+ while (bot) {
+ if (callback.call(thisArg, bot) === false) {
+ return this;
+ }
+ bot = bot.next;
+ }
+ return this;
+ };
+ /*\
+ * Paper.getElementsByPoint
+ [ method ]
+ **
+ * Returns set of elements that have common point inside
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (object) @Set
+ \*/
+ paperproto.getElementsByPoint = function (x, y) {
+ var set = this.set();
+ this.forEach(function (el) {
+ if (el.isPointInside(x, y)) {
+ set.push(el);
+ }
+ });
+ return set;
+ };
+ function x_y() {
+ return this.x + S + this.y;
+ }
+ function x_y_w_h() {
+ return this.x + S + this.y + S + this.width + " \xd7 " + this.height;
+ }
+ /*\
+ * Element.isPointInside
+ [ method ]
+ **
+ * Determine if given point is inside this element’s shape
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (boolean) `true` if point inside the shape
+ \*/
+ elproto.isPointInside = function (x, y) {
+ var rp = this.realPath = getPath[this.type](this);
+ if (this.attr('transform') && this.attr('transform').length) {
+ rp = R.transformPath(rp, this.attr('transform'));
+ }
+ return R.isPointInsidePath(rp, x, y);
+ };
+ /*\
+ * Element.getBBox
+ [ method ]
+ **
+ * Return bounding box for a given element
+ **
+ > Parameters
+ **
+ - isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`.
+ = (object) Bounding box object:
+ o {
+ o x: (number) top left corner x
+ o y: (number) top left corner y
+ o x2: (number) bottom right corner x
+ o y2: (number) bottom right corner y
+ o width: (number) width
+ o height: (number) height
+ o }
+ \*/
+ elproto.getBBox = function (isWithoutTransform) {
+ if (this.removed) {
+ return {};
+ }
+ var _ = this._;
+ if (isWithoutTransform) {
+ if (_.dirty || !_.bboxwt) {
+ this.realPath = getPath[this.type](this);
+ _.bboxwt = pathDimensions(this.realPath);
+ _.bboxwt.toString = x_y_w_h;
+ _.dirty = 0;
+ }
+ return _.bboxwt;
+ }
+ if (_.dirty || _.dirtyT || !_.bbox) {
+ if (_.dirty || !this.realPath) {
+ _.bboxwt = 0;
+ this.realPath = getPath[this.type](this);
+ }
+ _.bbox = pathDimensions(mapPath(this.realPath, this.matrix));
+ _.bbox.toString = x_y_w_h;
+ _.dirty = _.dirtyT = 0;
+ }
+ return _.bbox;
+ };
+ /*\
+ * Element.clone
+ [ method ]
+ **
+ = (object) clone of a given element
+ **
+ \*/
+ elproto.clone = function () {
+ if (this.removed) {
+ return null;
+ }
+ var out = this.paper[this.type]().attr(this.attr());
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Element.glow
+ [ method ]
+ **
+ * Return set of elements that create glow-like effect around given element. See @Paper.set.
+ *
+ * Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself.
+ **
+ > Parameters
+ **
+ - glow (object) #optional parameters object with all properties optional:
+ o {
+ o width (number) size of the glow, default is `10`
+ o fill (boolean) will it be filled, default is `false`
+ o opacity (number) opacity, default is `0.5`
+ o offsetx (number) horizontal offset, default is `0`
+ o offsety (number) vertical offset, default is `0`
+ o color (string) glow colour, default is `black`
+ o }
+ = (object) @Paper.set of elements that represents glow
+ \*/
+ elproto.glow = function (glow) {
+ if (this.type == "text") {
+ return null;
+ }
+ glow = glow || {};
+ var s = {
+ width: (glow.width || 10) + (+this.attr("stroke-width") || 1),
+ fill: glow.fill || false,
+ opacity: glow.opacity || .5,
+ offsetx: glow.offsetx || 0,
+ offsety: glow.offsety || 0,
+ color: glow.color || "#000"
+ },
+ c = s.width / 2,
+ r = this.paper,
+ out = r.set(),
+ path = this.realPath || getPath[this.type](this);
+ path = this.matrix ? mapPath(path, this.matrix) : path;
+ for (var i = 1; i < c + 1; i++) {
+ out.push(r.path(path).attr({
+ stroke: s.color,
+ fill: s.fill ? s.color : "none",
+ "stroke-linejoin": "round",
+ "stroke-linecap": "round",
+ "stroke-width": +(s.width / c * i).toFixed(3),
+ opacity: +(s.opacity / c).toFixed(3)
+ }));
+ }
+ return out.insertBefore(this).translate(s.offsetx, s.offsety);
+ };
+ var curveslengths = {},
+ getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
+ if (length == null) {
+ return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
+ } else {
+ return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
+ }
+ },
+ getLengthFactory = function (istotal, subpath) {
+ return function (path, length, onlystart) {
+ path = path2curve(path);
+ var x, y, p, l, sp = "", subpaths = {}, point,
+ len = 0;
+ for (var i = 0, ii = path.length; i < ii; i++) {
+ p = path[i];
+ if (p[0] == "M") {
+ x = +p[1];
+ y = +p[2];
+ } else {
+ l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ if (len + l > length) {
+ if (subpath && !subpaths.start) {
+ point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+ sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
+ if (onlystart) {return sp;}
+ subpaths.start = sp;
+ sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join();
+ len += l;
+ x = +p[5];
+ y = +p[6];
+ continue;
+ }
+ if (!istotal && !subpath) {
+ point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+ return {x: point.x, y: point.y, alpha: point.alpha};
+ }
+ }
+ len += l;
+ x = +p[5];
+ y = +p[6];
+ }
+ sp += p.shift() + p;
+ }
+ subpaths.end = sp;
+ point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
+ point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
+ return point;
+ };
+ };
+ var getTotalLength = getLengthFactory(1),
+ getPointAtLength = getLengthFactory(),
+ getSubpathsAtLength = getLengthFactory(0, 1);
+ /*\
+ * Raphael.getTotalLength
+ [ method ]
+ **
+ * Returns length of the given path in pixels.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string.
+ **
+ = (number) length.
+ \*/
+ R.getTotalLength = getTotalLength;
+ /*\
+ * Raphael.getPointAtLength
+ [ method ]
+ **
+ * Return coordinates of the point located at the given length on the given path.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string
+ - length (number)
+ **
+ = (object) representation of the point:
+ o {
+ o x: (number) x coordinate
+ o y: (number) y coordinate
+ o alpha: (number) angle of derivative
+ o }
+ \*/
+ R.getPointAtLength = getPointAtLength;
+ /*\
+ * Raphael.getSubpath
+ [ method ]
+ **
+ * Return subpath of a given path from given length to given length.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string
+ - from (number) position of the start of the segment
+ - to (number) position of the end of the segment
+ **
+ = (string) pathstring for the segment
+ \*/
+ R.getSubpath = function (path, from, to) {
+ if (this.getTotalLength(path) - to < 1e-6) {
+ return getSubpathsAtLength(path, from).end;
+ }
+ var a = getSubpathsAtLength(path, to, 1);
+ return from ? getSubpathsAtLength(a, from).end : a;
+ };
+ /*\
+ * Element.getTotalLength
+ [ method ]
+ **
+ * Returns length of the path in pixels. Only works for element of “path” type.
+ = (number) length.
+ \*/
+ elproto.getTotalLength = function () {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ if (this.node.getTotalLength) {
+ return this.node.getTotalLength();
+ }
+
+ return getTotalLength(path);
+ };
+ /*\
+ * Element.getPointAtLength
+ [ method ]
+ **
+ * Return coordinates of the point located at the given length on the given path. Only works for element of “path” type.
+ **
+ > Parameters
+ **
+ - length (number)
+ **
+ = (object) representation of the point:
+ o {
+ o x: (number) x coordinate
+ o y: (number) y coordinate
+ o alpha: (number) angle of derivative
+ o }
+ \*/
+ elproto.getPointAtLength = function (length) {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ return getPointAtLength(path, length);
+ };
+ /*\
+ * Element.getPath
+ [ method ]
+ **
+ * Returns path of the element. Only works for elements of “path” type and simple elements like circle.
+ = (object) path
+ **
+ \*/
+ elproto.getPath = function () {
+ var path,
+ getPath = R._getPath[this.type];
+
+ if (this.type == "text" || this.type == "set") {
+ return;
+ }
+
+ if (getPath) {
+ path = getPath(this);
+ }
+
+ return path;
+ };
+ /*\
+ * Element.getSubpath
+ [ method ]
+ **
+ * Return subpath of a given element from given length to given length. Only works for element of “path” type.
+ **
+ > Parameters
+ **
+ - from (number) position of the start of the segment
+ - to (number) position of the end of the segment
+ **
+ = (string) pathstring for the segment
+ \*/
+ elproto.getSubpath = function (from, to) {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ return R.getSubpath(path, from, to);
+ };
+ /*\
+ * Raphael.easing_formulas
+ [ property ]
+ **
+ * Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing:
+ # <ul>
+ # <li>“linear”</li>
+ # <li>“&lt;” or “easeIn” or “ease-in”</li>
+ # <li>“>” or “easeOut” or “ease-out”</li>
+ # <li>“&lt;>” or “easeInOut” or “ease-in-out”</li>
+ # <li>“backIn” or “back-in”</li>
+ # <li>“backOut” or “back-out”</li>
+ # <li>“elastic”</li>
+ # <li>“bounce”</li>
+ # </ul>
+ # <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p>
+ \*/
+ var ef = R.easing_formulas = {
+ linear: function (n) {
+ return n;
+ },
+ "<": function (n) {
+ return pow(n, 1.7);
+ },
+ ">": function (n) {
+ return pow(n, .48);
+ },
+ "<>": function (n) {
+ var q = .48 - n / 1.04,
+ Q = math.sqrt(.1734 + q * q),
+ x = Q - q,
+ X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
+ y = -Q - q,
+ Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
+ t = X + Y + .5;
+ return (1 - t) * 3 * t * t + t * t * t;
+ },
+ backIn: function (n) {
+ var s = 1.70158;
+ return n * n * ((s + 1) * n - s);
+ },
+ backOut: function (n) {
+ n = n - 1;
+ var s = 1.70158;
+ return n * n * ((s + 1) * n + s) + 1;
+ },
+ elastic: function (n) {
+ if (n == !!n) {
+ return n;
+ }
+ return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1;
+ },
+ bounce: function (n) {
+ var s = 7.5625,
+ p = 2.75,
+ l;
+ if (n < (1 / p)) {
+ l = s * n * n;
+ } else {
+ if (n < (2 / p)) {
+ n -= (1.5 / p);
+ l = s * n * n + .75;
+ } else {
+ if (n < (2.5 / p)) {
+ n -= (2.25 / p);
+ l = s * n * n + .9375;
+ } else {
+ n -= (2.625 / p);
+ l = s * n * n + .984375;
+ }
+ }
+ }
+ return l;
+ }
+ };
+ ef.easeIn = ef["ease-in"] = ef["<"];
+ ef.easeOut = ef["ease-out"] = ef[">"];
+ ef.easeInOut = ef["ease-in-out"] = ef["<>"];
+ ef["back-in"] = ef.backIn;
+ ef["back-out"] = ef.backOut;
+
+ var animationElements = [],
+ requestAnimFrame = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function (callback) {
+ setTimeout(callback, 16);
+ },
+ animation = function () {
+ var Now = +new Date,
+ l = 0;
+ for (; l < animationElements.length; l++) {
+ var e = animationElements[l];
+ if (e.el.removed || e.paused) {
+ continue;
+ }
+ var time = Now - e.start,
+ ms = e.ms,
+ easing = e.easing,
+ from = e.from,
+ diff = e.diff,
+ to = e.to,
+ t = e.t,
+ that = e.el,
+ set = {},
+ now,
+ init = {},
+ key;
+ if (e.initstatus) {
+ time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms;
+ e.status = e.initstatus;
+ delete e.initstatus;
+ e.stop && animationElements.splice(l--, 1);
+ } else {
+ e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top;
+ }
+ if (time < 0) {
+ continue;
+ }
+ if (time < ms) {
+ var pos = easing(time / ms);
+ for (var attr in from) if (from[has](attr)) {
+ switch (availableAnimAttrs[attr]) {
+ case nu:
+ now = +from[attr] + pos * ms * diff[attr];
+ break;
+ case "colour":
+ now = "rgb(" + [
+ upto255(round(from[attr].r + pos * ms * diff[attr].r)),
+ upto255(round(from[attr].g + pos * ms * diff[attr].g)),
+ upto255(round(from[attr].b + pos * ms * diff[attr].b))
+ ].join(",") + ")";
+ break;
+ case "path":
+ now = [];
+ for (var i = 0, ii = from[attr].length; i < ii; i++) {
+ now[i] = [from[attr][i][0]];
+ for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
+ now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
+ }
+ now[i] = now[i].join(S);
+ }
+ now = now.join(S);
+ break;
+ case "transform":
+ if (diff[attr].real) {
+ now = [];
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ now[i] = [from[attr][i][0]];
+ for (j = 1, jj = from[attr][i].length; j < jj; j++) {
+ now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j];
+ }
+ }
+ } else {
+ var get = function (i) {
+ return +from[attr][i] + pos * ms * diff[attr][i];
+ };
+ // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]];
+ now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]];
+ }
+ break;
+ case "csv":
+ if (attr == "clip-rect") {
+ now = [];
+ i = 4;
+ while (i--) {
+ now[i] = +from[attr][i] + pos * ms * diff[attr][i];
+ }
+ }
+ break;
+ default:
+ var from2 = [][concat](from[attr]);
+ now = [];
+ i = that.paper.customAttributes[attr].length;
+ while (i--) {
+ now[i] = +from2[i] + pos * ms * diff[attr][i];
+ }
+ break;
+ }
+ set[attr] = now;
+ }
+ that.attr(set);
+ (function (id, that, anim) {
+ setTimeout(function () {
+ eve("raphael.anim.frame." + id, that, anim);
+ });
+ })(that.id, that, e.anim);
+ } else {
+ (function(f, el, a) {
+ setTimeout(function() {
+ eve("raphael.anim.frame." + el.id, el, a);
+ eve("raphael.anim.finish." + el.id, el, a);
+ R.is(f, "function") && f.call(el);
+ });
+ })(e.callback, that, e.anim);
+ that.attr(to);
+ animationElements.splice(l--, 1);
+ if (e.repeat > 1 && !e.next) {
+ for (key in to) if (to[has](key)) {
+ init[key] = e.totalOrigin[key];
+ }
+ e.el.attr(init);
+ runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1);
+ }
+ if (e.next && !e.stop) {
+ runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat);
+ }
+ }
+ }
+ R.svg && that && that.paper && that.paper.safari();
+ animationElements.length && requestAnimFrame(animation);
+ },
+ upto255 = function (color) {
+ return color > 255 ? 255 : color < 0 ? 0 : color;
+ };
+ /*\
+ * Element.animateWith
+ [ method ]
+ **
+ * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element.
+ **
+ > Parameters
+ **
+ - el (object) element to sync with
+ - anim (object) animation to sync with
+ - params (object) #optional final attributes for the element, see also @Element.attr
+ - ms (number) #optional number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ * or
+ - element (object) element to sync with
+ - anim (object) animation to sync with
+ - animation (object) #optional animation object, see @Raphael.animation
+ **
+ = (object) original element
+ \*/
+ elproto.animateWith = function (el, anim, params, ms, easing, callback) {
+ var element = this;
+ if (element.removed) {
+ callback && callback.call(element);
+ return element;
+ }
+ var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback),
+ x, y;
+ runAnimation(a, element, a.percents[0], null, element.attr());
+ for (var i = 0, ii = animationElements.length; i < ii; i++) {
+ if (animationElements[i].anim == anim && animationElements[i].el == el) {
+ animationElements[ii - 1].start = animationElements[i].start;
+ break;
+ }
+ }
+ return element;
+ //
+ //
+ // var a = params ? R.animation(params, ms, easing, callback) : anim,
+ // status = element.status(anim);
+ // return this.animate(a).status(a, status * anim.ms / a.ms);
+ };
+ function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) {
+ var cx = 3 * p1x,
+ bx = 3 * (p2x - p1x) - cx,
+ ax = 1 - cx - bx,
+ cy = 3 * p1y,
+ by = 3 * (p2y - p1y) - cy,
+ ay = 1 - cy - by;
+ function sampleCurveX(t) {
+ return ((ax * t + bx) * t + cx) * t;
+ }
+ function solve(x, epsilon) {
+ var t = solveCurveX(x, epsilon);
+ return ((ay * t + by) * t + cy) * t;
+ }
+ function solveCurveX(x, epsilon) {
+ var t0, t1, t2, x2, d2, i;
+ for(t2 = x, i = 0; i < 8; i++) {
+ x2 = sampleCurveX(t2) - x;
+ if (abs(x2) < epsilon) {
+ return t2;
+ }
+ d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
+ if (abs(d2) < 1e-6) {
+ break;
+ }
+ t2 = t2 - x2 / d2;
+ }
+ t0 = 0;
+ t1 = 1;
+ t2 = x;
+ if (t2 < t0) {
+ return t0;
+ }
+ if (t2 > t1) {
+ return t1;
+ }
+ while (t0 < t1) {
+ x2 = sampleCurveX(t2);
+ if (abs(x2 - x) < epsilon) {
+ return t2;
+ }
+ if (x > x2) {
+ t0 = t2;
+ } else {
+ t1 = t2;
+ }
+ t2 = (t1 - t0) / 2 + t0;
+ }
+ return t2;
+ }
+ return solve(t, 1 / (200 * duration));
+ }
+ elproto.onAnimation = function (f) {
+ f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id);
+ return this;
+ };
+ function Animation(anim, ms) {
+ var percents = [],
+ newAnim = {};
+ this.ms = ms;
+ this.times = 1;
+ if (anim) {
+ for (var attr in anim) if (anim[has](attr)) {
+ newAnim[toFloat(attr)] = anim[attr];
+ percents.push(toFloat(attr));
+ }
+ percents.sort(sortByNumber);
+ }
+ this.anim = newAnim;
+ this.top = percents[percents.length - 1];
+ this.percents = percents;
+ }
+ /*\
+ * Animation.delay
+ [ method ]
+ **
+ * Creates a copy of existing animation object with given delay.
+ **
+ > Parameters
+ **
+ - delay (number) number of ms to pass between animation start and actual animation
+ **
+ = (object) new altered Animation object
+ | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3);
+ | circle1.animate(anim); // run the given animation immediately
+ | circle2.animate(anim.delay(500)); // run the given animation after 500 ms
+ \*/
+ Animation.prototype.delay = function (delay) {
+ var a = new Animation(this.anim, this.ms);
+ a.times = this.times;
+ a.del = +delay || 0;
+ return a;
+ };
+ /*\
+ * Animation.repeat
+ [ method ]
+ **
+ * Creates a copy of existing animation object with given repetition.
+ **
+ > Parameters
+ **
+ - repeat (number) number iterations of animation. For infinite animation pass `Infinity`
+ **
+ = (object) new altered Animation object
+ \*/
+ Animation.prototype.repeat = function (times) {
+ var a = new Animation(this.anim, this.ms);
+ a.del = this.del;
+ a.times = math.floor(mmax(times, 0)) || 1;
+ return a;
+ };
+ function runAnimation(anim, element, percent, status, totalOrigin, times) {
+ percent = toFloat(percent);
+ var params,
+ isInAnim,
+ isInAnimSet,
+ percents = [],
+ next,
+ prev,
+ timestamp,
+ ms = anim.ms,
+ from = {},
+ to = {},
+ diff = {};
+ if (status) {
+ for (i = 0, ii = animationElements.length; i < ii; i++) {
+ var e = animationElements[i];
+ if (e.el.id == element.id && e.anim == anim) {
+ if (e.percent != percent) {
+ animationElements.splice(i, 1);
+ isInAnimSet = 1;
+ } else {
+ isInAnim = e;
+ }
+ element.attr(e.totalOrigin);
+ break;
+ }
+ }
+ } else {
+ status = +to; // NaN
+ }
+ for (var i = 0, ii = anim.percents.length; i < ii; i++) {
+ if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) {
+ percent = anim.percents[i];
+ prev = anim.percents[i - 1] || 0;
+ ms = ms / anim.top * (percent - prev);
+ next = anim.percents[i + 1];
+ params = anim.anim[percent];
+ break;
+ } else if (status) {
+ element.attr(anim.anim[anim.percents[i]]);
+ }
+ }
+ if (!params) {
+ return;
+ }
+ if (!isInAnim) {
+ for (var attr in params) if (params[has](attr)) {
+ if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) {
+ from[attr] = element.attr(attr);
+ (from[attr] == null) && (from[attr] = availableAttrs[attr]);
+ to[attr] = params[attr];
+ switch (availableAnimAttrs[attr]) {
+ case nu:
+ diff[attr] = (to[attr] - from[attr]) / ms;
+ break;
+ case "colour":
+ from[attr] = R.getRGB(from[attr]);
+ var toColour = R.getRGB(to[attr]);
+ diff[attr] = {
+ r: (toColour.r - from[attr].r) / ms,
+ g: (toColour.g - from[attr].g) / ms,
+ b: (toColour.b - from[attr].b) / ms
+ };
+ break;
+ case "path":
+ var pathes = path2curve(from[attr], to[attr]),
+ toPath = pathes[1];
+ from[attr] = pathes[0];
+ diff[attr] = [];
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ diff[attr][i] = [0];
+ for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
+ diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
+ }
+ }
+ break;
+ case "transform":
+ var _ = element._,
+ eq = equaliseTransform(_[attr], to[attr]);
+ if (eq) {
+ from[attr] = eq.from;
+ to[attr] = eq.to;
+ diff[attr] = [];
+ diff[attr].real = true;
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ diff[attr][i] = [from[attr][i][0]];
+ for (j = 1, jj = from[attr][i].length; j < jj; j++) {
+ diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms;
+ }
+ }
+ } else {
+ var m = (element.matrix || new Matrix),
+ to2 = {
+ _: {transform: _.transform},
+ getBBox: function () {
+ return element.getBBox(1);
+ }
+ };
+ from[attr] = [
+ m.a,
+ m.b,
+ m.c,
+ m.d,
+ m.e,
+ m.f
+ ];
+ extractTransform(to2, to[attr]);
+ to[attr] = to2._.transform;
+ diff[attr] = [
+ (to2.matrix.a - m.a) / ms,
+ (to2.matrix.b - m.b) / ms,
+ (to2.matrix.c - m.c) / ms,
+ (to2.matrix.d - m.d) / ms,
+ (to2.matrix.e - m.e) / ms,
+ (to2.matrix.f - m.f) / ms
+ ];
+ // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy];
+ // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }};
+ // extractTransform(to2, to[attr]);
+ // diff[attr] = [
+ // (to2._.sx - _.sx) / ms,
+ // (to2._.sy - _.sy) / ms,
+ // (to2._.deg - _.deg) / ms,
+ // (to2._.dx - _.dx) / ms,
+ // (to2._.dy - _.dy) / ms
+ // ];
+ }
+ break;
+ case "csv":
+ var values = Str(params[attr])[split](separator),
+ from2 = Str(from[attr])[split](separator);
+ if (attr == "clip-rect") {
+ from[attr] = from2;
+ diff[attr] = [];
+ i = from2.length;
+ while (i--) {
+ diff[attr][i] = (values[i] - from[attr][i]) / ms;
+ }
+ }
+ to[attr] = values;
+ break;
+ default:
+ values = [][concat](params[attr]);
+ from2 = [][concat](from[attr]);
+ diff[attr] = [];
+ i = element.paper.customAttributes[attr].length;
+ while (i--) {
+ diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms;
+ }
+ break;
+ }
+ }
+ }
+ var easing = params.easing,
+ easyeasy = R.easing_formulas[easing];
+ if (!easyeasy) {
+ easyeasy = Str(easing).match(bezierrg);
+ if (easyeasy && easyeasy.length == 5) {
+ var curve = easyeasy;
+ easyeasy = function (t) {
+ return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms);
+ };
+ } else {
+ easyeasy = pipe;
+ }
+ }
+ timestamp = params.start || anim.start || +new Date;
+ e = {
+ anim: anim,
+ percent: percent,
+ timestamp: timestamp,
+ start: timestamp + (anim.del || 0),
+ status: 0,
+ initstatus: status || 0,
+ stop: false,
+ ms: ms,
+ easing: easyeasy,
+ from: from,
+ diff: diff,
+ to: to,
+ el: element,
+ callback: params.callback,
+ prev: prev,
+ next: next,
+ repeat: times || anim.times,
+ origin: element.attr(),
+ totalOrigin: totalOrigin
+ };
+ animationElements.push(e);
+ if (status && !isInAnim && !isInAnimSet) {
+ e.stop = true;
+ e.start = new Date - ms * status;
+ if (animationElements.length == 1) {
+ return animation();
+ }
+ }
+ if (isInAnimSet) {
+ e.start = new Date - e.ms * status;
+ }
+ animationElements.length == 1 && requestAnimFrame(animation);
+ } else {
+ isInAnim.initstatus = status;
+ isInAnim.start = new Date - isInAnim.ms * status;
+ }
+ eve("raphael.anim.start." + element.id, element, anim);
+ }
+ /*\
+ * Raphael.animation
+ [ method ]
+ **
+ * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods.
+ * See also @Animation.delay and @Animation.repeat methods.
+ **
+ > Parameters
+ **
+ - params (object) final attributes for the element, see also @Element.attr
+ - ms (number) number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ **
+ = (object) @Animation
+ \*/
+ R.animation = function (params, ms, easing, callback) {
+ if (params instanceof Animation) {
+ return params;
+ }
+ if (R.is(easing, "function") || !easing) {
+ callback = callback || easing || null;
+ easing = null;
+ }
+ params = Object(params);
+ ms = +ms || 0;
+ var p = {},
+ json,
+ attr;
+ for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) {
+ json = true;
+ p[attr] = params[attr];
+ }
+ if (!json) {
+ // if percent-like syntax is used and end-of-all animation callback used
+ if(callback){
+ // find the last one
+ var lastKey = 0;
+ for(var i in params){
+ var percent = toInt(i);
+ if(params[has](i) && percent > lastKey){
+ lastKey = percent;
+ }
+ }
+ lastKey += '%';
+ // if already defined callback in the last keyframe, skip
+ !params[lastKey].callback && (params[lastKey].callback = callback);
+ }
+ return new Animation(params, ms);
+ } else {
+ easing && (p.easing = easing);
+ callback && (p.callback = callback);
+ return new Animation({100: p}, ms);
+ }
+ };
+ /*\
+ * Element.animate
+ [ method ]
+ **
+ * Creates and starts animation for given element.
+ **
+ > Parameters
+ **
+ - params (object) final attributes for the element, see also @Element.attr
+ - ms (number) number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ * or
+ - animation (object) animation object, see @Raphael.animation
+ **
+ = (object) original element
+ \*/
+ elproto.animate = function (params, ms, easing, callback) {
+ var element = this;
+ if (element.removed) {
+ callback && callback.call(element);
+ return element;
+ }
+ var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback);
+ runAnimation(anim, element, anim.percents[0], null, element.attr());
+ return element;
+ };
+ /*\
+ * Element.setTime
+ [ method ]
+ **
+ * Sets the status of animation of the element in milliseconds. Similar to @Element.status method.
+ **
+ > Parameters
+ **
+ - anim (object) animation object
+ - value (number) number of milliseconds from the beginning of the animation
+ **
+ = (object) original element if `value` is specified
+ * Note, that during animation following events are triggered:
+ *
+ * On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`.
+ \*/
+ elproto.setTime = function (anim, value) {
+ if (anim && value != null) {
+ this.status(anim, mmin(value, anim.ms) / anim.ms);
+ }
+ return this;
+ };
+ /*\
+ * Element.status
+ [ method ]
+ **
+ * Gets or sets the status of animation of the element.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ - value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position.
+ **
+ = (number) status
+ * or
+ = (array) status if `anim` is not specified. Array of objects in format:
+ o {
+ o anim: (object) animation object
+ o status: (number) status
+ o }
+ * or
+ = (object) original element if `value` is specified
+ \*/
+ elproto.status = function (anim, value) {
+ var out = [],
+ i = 0,
+ len,
+ e;
+ if (value != null) {
+ runAnimation(anim, this, -1, mmin(value, 1));
+ return this;
+ } else {
+ len = animationElements.length;
+ for (; i < len; i++) {
+ e = animationElements[i];
+ if (e.el.id == this.id && (!anim || e.anim == anim)) {
+ if (anim) {
+ return e.status;
+ }
+ out.push({
+ anim: e.anim,
+ status: e.status
+ });
+ }
+ }
+ if (anim) {
+ return 0;
+ }
+ return out;
+ }
+ };
+ /*\
+ * Element.pause
+ [ method ]
+ **
+ * Stops animation of the element with ability to resume it later on.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.pause = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) {
+ animationElements[i].paused = true;
+ }
+ }
+ return this;
+ };
+ /*\
+ * Element.resume
+ [ method ]
+ **
+ * Resumes animation if it was paused with @Element.pause method.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.resume = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ var e = animationElements[i];
+ if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) {
+ delete e.paused;
+ this.status(e.anim, e.status);
+ }
+ }
+ return this;
+ };
+ /*\
+ * Element.stop
+ [ method ]
+ **
+ * Stops animation of the element.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.stop = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) {
+ animationElements.splice(i--, 1);
+ }
+ }
+ return this;
+ };
+ function stopAnimation(paper) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) {
+ animationElements.splice(i--, 1);
+ }
+ }
+ eve.on("raphael.remove", stopAnimation);
+ eve.on("raphael.clear", stopAnimation);
+ elproto.toString = function () {
+ return "Rapha\xebl\u2019s object";
+ };
+
+ // Set
+ var Set = function (items) {
+ this.items = [];
+ this.length = 0;
+ this.type = "set";
+ if (items) {
+ for (var i = 0, ii = items.length; i < ii; i++) {
+ if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) {
+ this[this.items.length] = this.items[this.items.length] = items[i];
+ this.length++;
+ }
+ }
+ }
+ },
+ setproto = Set.prototype;
+ /*\
+ * Set.push
+ [ method ]
+ **
+ * Adds each argument to the current set.
+ = (object) original element
+ \*/
+ setproto.push = function () {
+ var item,
+ len;
+ for (var i = 0, ii = arguments.length; i < ii; i++) {
+ item = arguments[i];
+ if (item && (item.constructor == elproto.constructor || item.constructor == Set)) {
+ len = this.items.length;
+ this[len] = this.items[len] = item;
+ this.length++;
+ }
+ }
+ return this;
+ };
+ /*\
+ * Set.pop
+ [ method ]
+ **
+ * Removes last element and returns it.
+ = (object) element
+ \*/
+ setproto.pop = function () {
+ this.length && delete this[this.length--];
+ return this.items.pop();
+ };
+ /*\
+ * Set.forEach
+ [ method ]
+ **
+ * Executes given function for each element in the set.
+ *
+ * If function returns `false` it will stop loop running.
+ **
+ > Parameters
+ **
+ - callback (function) function to run
+ - thisArg (object) context object for the callback
+ = (object) Set object
+ \*/
+ setproto.forEach = function (callback, thisArg) {
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ if (callback.call(thisArg, this.items[i], i) === false) {
+ return this;
+ }
+ }
+ return this;
+ };
+ for (var method in elproto) if (elproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname][apply](el, arg);
+ });
+ };
+ })(method);
+ }
+ setproto.attr = function (name, value) {
+ if (name && R.is(name, array) && R.is(name[0], "object")) {
+ for (var j = 0, jj = name.length; j < jj; j++) {
+ this.items[j].attr(name[j]);
+ }
+ } else {
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ this.items[i].attr(name, value);
+ }
+ }
+ return this;
+ };
+ /*\
+ * Set.clear
+ [ method ]
+ **
+ * Removes all elements from the set
+ \*/
+ setproto.clear = function () {
+ while (this.length) {
+ this.pop();
+ }
+ };
+ /*\
+ * Set.splice
+ [ method ]
+ **
+ * Removes given element from the set
+ **
+ > Parameters
+ **
+ - index (number) position of the deletion
+ - count (number) number of element to remove
+ - insertion… (object) #optional elements to insert
+ = (object) set elements that were deleted
+ \*/
+ setproto.splice = function (index, count, insertion) {
+ index = index < 0 ? mmax(this.length + index, 0) : index;
+ count = mmax(0, mmin(this.length - index, count));
+ var tail = [],
+ todel = [],
+ args = [],
+ i;
+ for (i = 2; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ for (i = 0; i < count; i++) {
+ todel.push(this[index + i]);
+ }
+ for (; i < this.length - index; i++) {
+ tail.push(this[index + i]);
+ }
+ var arglen = args.length;
+ for (i = 0; i < arglen + tail.length; i++) {
+ this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen];
+ }
+ i = this.items.length = this.length -= count - arglen;
+ while (this[i]) {
+ delete this[i++];
+ }
+ return new Set(todel);
+ };
+ /*\
+ * Set.exclude
+ [ method ]
+ **
+ * Removes given element from the set
+ **
+ > Parameters
+ **
+ - element (object) element to remove
+ = (boolean) `true` if object was found & removed from the set
+ \*/
+ setproto.exclude = function (el) {
+ for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) {
+ this.splice(i, 1);
+ return true;
+ }
+ };
+ setproto.animate = function (params, ms, easing, callback) {
+ (R.is(easing, "function") || !easing) && (callback = easing || null);
+ var len = this.items.length,
+ i = len,
+ item,
+ set = this,
+ collector;
+ if (!len) {
+ return this;
+ }
+ callback && (collector = function () {
+ !--len && callback.call(set);
+ });
+ easing = R.is(easing, string) ? easing : collector;
+ var anim = R.animation(params, ms, easing, collector);
+ item = this.items[--i].animate(anim);
+ while (i--) {
+ this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim);
+ (this.items[i] && !this.items[i].removed) || len--;
+ }
+ return this;
+ };
+ setproto.insertAfter = function (el) {
+ var i = this.items.length;
+ while (i--) {
+ this.items[i].insertAfter(el);
+ }
+ return this;
+ };
+ setproto.getBBox = function () {
+ var x = [],
+ y = [],
+ x2 = [],
+ y2 = [];
+ for (var i = this.items.length; i--;) if (!this.items[i].removed) {
+ var box = this.items[i].getBBox();
+ x.push(box.x);
+ y.push(box.y);
+ x2.push(box.x + box.width);
+ y2.push(box.y + box.height);
+ }
+ x = mmin[apply](0, x);
+ y = mmin[apply](0, y);
+ x2 = mmax[apply](0, x2);
+ y2 = mmax[apply](0, y2);
+ return {
+ x: x,
+ y: y,
+ x2: x2,
+ y2: y2,
+ width: x2 - x,
+ height: y2 - y
+ };
+ };
+ setproto.clone = function (s) {
+ s = this.paper.set();
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ s.push(this.items[i].clone());
+ }
+ return s;
+ };
+ setproto.toString = function () {
+ return "Rapha\xebl\u2018s set";
+ };
+
+ setproto.glow = function(glowConfig) {
+ var ret = this.paper.set();
+ this.forEach(function(shape, index){
+ var g = shape.glow(glowConfig);
+ if(g != null){
+ g.forEach(function(shape2, index2){
+ ret.push(shape2);
+ });
+ }
+ });
+ return ret;
+ };
+
+
+ /*\
+ * Set.isPointInside
+ [ method ]
+ **
+ * Determine if given point is inside this set’s elements
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (boolean) `true` if point is inside any of the set's elements
+ \*/
+ setproto.isPointInside = function (x, y) {
+ var isPointInside = false;
+ this.forEach(function (el) {
+ if (el.isPointInside(x, y)) {
+ isPointInside = true;
+ return false; // stop loop
+ }
+ });
+ return isPointInside;
+ };
+
+ /*\
+ * Raphael.registerFont
+ [ method ]
+ **
+ * Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file.
+ * Returns original parameter, so it could be used with chaining.
+ # <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a>
+ **
+ > Parameters
+ **
+ - font (object) the font to register
+ = (object) the font you passed in
+ > Usage
+ | Cufon.registerFont(Raphael.registerFont({…}));
+ \*/
+ R.registerFont = function (font) {
+ if (!font.face) {
+ return font;
+ }
+ this.fonts = this.fonts || {};
+ var fontcopy = {
+ w: font.w,
+ face: {},
+ glyphs: {}
+ },
+ family = font.face["font-family"];
+ for (var prop in font.face) if (font.face[has](prop)) {
+ fontcopy.face[prop] = font.face[prop];
+ }
+ if (this.fonts[family]) {
+ this.fonts[family].push(fontcopy);
+ } else {
+ this.fonts[family] = [fontcopy];
+ }
+ if (!font.svg) {
+ fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
+ for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
+ var path = font.glyphs[glyph];
+ fontcopy.glyphs[glyph] = {
+ w: path.w,
+ k: {},
+ d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) {
+ return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
+ }) + "z"
+ };
+ if (path.k) {
+ for (var k in path.k) if (path[has](k)) {
+ fontcopy.glyphs[glyph].k[k] = path.k[k];
+ }
+ }
+ }
+ }
+ return font;
+ };
+ /*\
+ * Paper.getFont
+ [ method ]
+ **
+ * Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”.
+ **
+ > Parameters
+ **
+ - family (string) font family name or any word from it
+ - weight (string) #optional font weight
+ - style (string) #optional font style
+ - stretch (string) #optional font stretch
+ = (object) the font object
+ > Usage
+ | paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
+ \*/
+ paperproto.getFont = function (family, weight, style, stretch) {
+ stretch = stretch || "normal";
+ style = style || "normal";
+ weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
+ if (!R.fonts) {
+ return;
+ }
+ var font = R.fonts[family];
+ if (!font) {
+ var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
+ for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
+ if (name.test(fontName)) {
+ font = R.fonts[fontName];
+ break;
+ }
+ }
+ }
+ var thefont;
+ if (font) {
+ for (var i = 0, ii = font.length; i < ii; i++) {
+ thefont = font[i];
+ if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
+ break;
+ }
+ }
+ }
+ return thefont;
+ };
+ /*\
+ * Paper.print
+ [ method ]
+ **
+ * Creates path that represent given text written using given font at given position with given size.
+ * Result of the method is path element that contains whole text as a separate path.
+ **
+ > Parameters
+ **
+ - x (number) x position of the text
+ - y (number) y position of the text
+ - string (string) text to print
+ - font (object) font object, see @Paper.getFont
+ - size (number) #optional size of the font, default is `16`
+ - origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"`
+ - letter_spacing (number) #optional number in range `-1..1`, default is `0`
+ - line_spacing (number) #optional number in range `1..3`, default is `1`
+ = (object) resulting path element, which consist of all letters
+ > Usage
+ | var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"});
+ \*/
+ paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) {
+ origin = origin || "middle"; // baseline|middle
+ letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1);
+ line_spacing = mmax(mmin(line_spacing || 1, 3), 1);
+ var letters = Str(string)[split](E),
+ shift = 0,
+ notfirst = 0,
+ path = E,
+ scale;
+ R.is(font, "string") && (font = this.getFont(font));
+ if (font) {
+ scale = (size || 16) / font.face["units-per-em"];
+ var bb = font.face.bbox[split](separator),
+ top = +bb[0],
+ lineHeight = bb[3] - bb[1],
+ shifty = 0,
+ height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2);
+ for (var i = 0, ii = letters.length; i < ii; i++) {
+ if (letters[i] == "\n") {
+ shift = 0;
+ curr = 0;
+ notfirst = 0;
+ shifty += lineHeight * line_spacing;
+ } else {
+ var prev = notfirst && font.glyphs[letters[i - 1]] || {},
+ curr = font.glyphs[letters[i]];
+ shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0;
+ notfirst = 1;
+ }
+ if (curr && curr.d) {
+ path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]);
+ }
+ }
+ }
+ return this.path(path).attr({
+ fill: "#000",
+ stroke: "none"
+ });
+ };
+
+ /*\
+ * Paper.add
+ [ method ]
+ **
+ * Imports elements in JSON array in format `{type: type, <attributes>}`
+ **
+ > Parameters
+ **
+ - json (array)
+ = (object) resulting set of imported elements
+ > Usage
+ | paper.add([
+ | {
+ | type: "circle",
+ | cx: 10,
+ | cy: 10,
+ | r: 5
+ | },
+ | {
+ | type: "rect",
+ | x: 10,
+ | y: 10,
+ | width: 10,
+ | height: 10,
+ | fill: "#fc0"
+ | }
+ | ]);
+ \*/
+ paperproto.add = function (json) {
+ if (R.is(json, "array")) {
+ var res = this.set(),
+ i = 0,
+ ii = json.length,
+ j;
+ for (; i < ii; i++) {
+ j = json[i] || {};
+ elements[has](j.type) && res.push(this[j.type]().attr(j));
+ }
+ }
+ return res;
+ };
+
+ /*\
+ * Raphael.format
+ [ method ]
+ **
+ * Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument.
+ **
+ > Parameters
+ **
+ - token (string) string to format
+ - … (string) rest of arguments will be treated as parameters for replacement
+ = (string) formated string
+ > Usage
+ | var x = 10,
+ | y = 20,
+ | width = 40,
+ | height = 50;
+ | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
+ | paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width));
+ \*/
+ R.format = function (token, params) {
+ var args = R.is(params, array) ? [0][concat](params) : arguments;
+ token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) {
+ return args[++i] == null ? E : args[i];
+ }));
+ return token || E;
+ };
+ /*\
+ * Raphael.fullfill
+ [ method ]
+ **
+ * A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument.
+ **
+ > Parameters
+ **
+ - token (string) string to format
+ - json (object) object which properties will be used as a replacement
+ = (string) formated string
+ > Usage
+ | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
+ | paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", {
+ | x: 10,
+ | y: 20,
+ | dim: {
+ | width: 40,
+ | height: 50,
+ | "negative width": -40
+ | }
+ | }));
+ \*/
+ R.fullfill = (function () {
+ var tokenRegex = /\{([^\}]+)\}/g,
+ objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
+ replacer = function (all, key, obj) {
+ var res = obj;
+ key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) {
+ name = name || quotedName;
+ if (res) {
+ if (name in res) {
+ res = res[name];
+ }
+ typeof res == "function" && isFunc && (res = res());
+ }
+ });
+ res = (res == null || res == obj ? all : res) + "";
+ return res;
+ };
+ return function (str, obj) {
+ return String(str).replace(tokenRegex, function (all, key) {
+ return replacer(all, key, obj);
+ });
+ };
+ })();
+ /*\
+ * Raphael.ninja
+ [ method ]
+ **
+ * If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method.
+ * Beware, that in this case plugins could stop working, because they are depending on global variable existance.
+ **
+ = (object) Raphael object
+ > Usage
+ | (function (local_raphael) {
+ | var paper = local_raphael(10, 10, 320, 200);
+ | …
+ | })(Raphael.ninja());
+ \*/
+ R.ninja = function () {
+ oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael;
+ return R;
+ };
+ /*\
+ * Raphael.st
+ [ property (object) ]
+ **
+ * You can add your own method to elements and sets. It is wise to add a set method for each element method
+ * you added, so you will be able to call the same method on sets too.
+ **
+ * See also @Raphael.el.
+ > Usage
+ | Raphael.el.red = function () {
+ | this.attr({fill: "#f00"});
+ | };
+ | Raphael.st.red = function () {
+ | this.forEach(function (el) {
+ | el.red();
+ | });
+ | };
+ | // then use it
+ | paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red();
+ \*/
+ R.st = setproto;
+
+ eve.on("raphael.DOMload", function () {
+ loaded = true;
+ });
+
+ // Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
+ (function (doc, loaded, f) {
+ if (doc.readyState == null && doc.addEventListener){
+ doc.addEventListener(loaded, f = function () {
+ doc.removeEventListener(loaded, f, false);
+ doc.readyState = "complete";
+ }, false);
+ doc.readyState = "loading";
+ }
+ function isLoaded() {
+ (/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload");
+ }
+ isLoaded();
+ })(document, "DOMContentLoaded");
+
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ SVG Module │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function(){
+ if (!R.svg) {
+ return;
+ }
+ var has = "hasOwnProperty",
+ Str = String,
+ toFloat = parseFloat,
+ toInt = parseInt,
+ math = Math,
+ mmax = math.max,
+ abs = math.abs,
+ pow = math.pow,
+ separator = /[, ]+/,
+ eve = R.eve,
+ E = "",
+ S = " ";
+ var xlink = "http://www.w3.org/1999/xlink",
+ markers = {
+ block: "M5,0 0,2.5 5,5z",
+ classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z",
+ diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z",
+ open: "M6,1 1,3.5 6,6",
+ oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z"
+ },
+ markerCounter = {};
+ R.toString = function () {
+ return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
+ };
+ var $ = function (el, attr) {
+ if (attr) {
+ if (typeof el == "string") {
+ el = $(el);
+ }
+ for (var key in attr) if (attr[has](key)) {
+ if (key.substring(0, 6) == "xlink:") {
+ el.setAttributeNS(xlink, key.substring(6), Str(attr[key]));
+ } else {
+ el.setAttribute(key, Str(attr[key]));
+ }
+ }
+ } else {
+ el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el);
+ el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
+ }
+ return el;
+ },
+ addGradientFill = function (element, gradient) {
+ var type = "linear",
+ id = element.id + gradient,
+ fx = .5, fy = .5,
+ o = element.node,
+ SVG = element.paper,
+ s = o.style,
+ el = R._g.doc.getElementById(id);
+ if (!el) {
+ gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) {
+ type = "radial";
+ if (_fx && _fy) {
+ fx = toFloat(_fx);
+ fy = toFloat(_fy);
+ var dir = ((fy > .5) * 2 - 1);
+ pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
+ (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
+ fy != .5 &&
+ (fy = fy.toFixed(5) - 1e-5 * dir);
+ }
+ return E;
+ });
+ gradient = gradient.split(/\s*\-\s*/);
+ if (type == "linear") {
+ var angle = gradient.shift();
+ angle = -toFloat(angle);
+ if (isNaN(angle)) {
+ return null;
+ }
+ var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))],
+ max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1);
+ vector[2] *= max;
+ vector[3] *= max;
+ if (vector[2] < 0) {
+ vector[0] = -vector[2];
+ vector[2] = 0;
+ }
+ if (vector[3] < 0) {
+ vector[1] = -vector[3];
+ vector[3] = 0;
+ }
+ }
+ var dots = R._parseDots(gradient);
+ if (!dots) {
+ return null;
+ }
+ id = id.replace(/[\(\)\s,\xb0#]/g, "_");
+
+ if (element.gradient && id != element.gradient.id) {
+ SVG.defs.removeChild(element.gradient);
+ delete element.gradient;
+ }
+
+ if (!element.gradient) {
+ el = $(type + "Gradient", {id: id});
+ element.gradient = el;
+ $(el, type == "radial" ? {
+ fx: fx,
+ fy: fy
+ } : {
+ x1: vector[0],
+ y1: vector[1],
+ x2: vector[2],
+ y2: vector[3],
+ gradientTransform: element.matrix.invert()
+ });
+ SVG.defs.appendChild(el);
+ for (var i = 0, ii = dots.length; i < ii; i++) {
+ el.appendChild($("stop", {
+ offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%",
+ "stop-color": dots[i].color || "#fff"
+ }));
+ }
+ }
+ }
+ $(o, {
+ fill: "url('" + document.location + "#" + id + "')",
+ opacity: 1,
+ "fill-opacity": 1
+ });
+ s.fill = E;
+ s.opacity = 1;
+ s.fillOpacity = 1;
+ return 1;
+ },
+ updatePosition = function (o) {
+ var bbox = o.getBBox(1);
+ $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
+ },
+ addArrow = function (o, value, isEnd) {
+ if (o.type == "path") {
+ var values = Str(value).toLowerCase().split("-"),
+ p = o.paper,
+ se = isEnd ? "end" : "start",
+ node = o.node,
+ attrs = o.attrs,
+ stroke = attrs["stroke-width"],
+ i = values.length,
+ type = "classic",
+ from,
+ to,
+ dx,
+ refX,
+ attr,
+ w = 3,
+ h = 3,
+ t = 5;
+ while (i--) {
+ switch (values[i]) {
+ case "block":
+ case "classic":
+ case "oval":
+ case "diamond":
+ case "open":
+ case "none":
+ type = values[i];
+ break;
+ case "wide": h = 5; break;
+ case "narrow": h = 2; break;
+ case "long": w = 5; break;
+ case "short": w = 2; break;
+ }
+ }
+ if (type == "open") {
+ w += 2;
+ h += 2;
+ t += 2;
+ dx = 1;
+ refX = isEnd ? 4 : 1;
+ attr = {
+ fill: "none",
+ stroke: attrs.stroke
+ };
+ } else {
+ refX = dx = w / 2;
+ attr = {
+ fill: attrs.stroke,
+ stroke: "none"
+ };
+ }
+ if (o._.arrows) {
+ if (isEnd) {
+ o._.arrows.endPath && markerCounter[o._.arrows.endPath]--;
+ o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--;
+ } else {
+ o._.arrows.startPath && markerCounter[o._.arrows.startPath]--;
+ o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--;
+ }
+ } else {
+ o._.arrows = {};
+ }
+ if (type != "none") {
+ var pathId = "raphael-marker-" + type,
+ markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id;
+ if (!R._g.doc.getElementById(pathId)) {
+ p.defs.appendChild($($("path"), {
+ "stroke-linecap": "round",
+ d: markers[type],
+ id: pathId
+ }));
+ markerCounter[pathId] = 1;
+ } else {
+ markerCounter[pathId]++;
+ }
+ var marker = R._g.doc.getElementById(markerId),
+ use;
+ if (!marker) {
+ marker = $($("marker"), {
+ id: markerId,
+ markerHeight: h,
+ markerWidth: w,
+ orient: "auto",
+ refX: refX,
+ refY: h / 2
+ });
+ use = $($("use"), {
+ "xlink:href": "#" + pathId,
+ transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")",
+ "stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4)
+ });
+ marker.appendChild(use);
+ p.defs.appendChild(marker);
+ markerCounter[markerId] = 1;
+ } else {
+ markerCounter[markerId]++;
+ use = marker.getElementsByTagName("use")[0];
+ }
+ $(use, attr);
+ var delta = dx * (type != "diamond" && type != "oval");
+ if (isEnd) {
+ from = o._.arrows.startdx * stroke || 0;
+ to = R.getTotalLength(attrs.path) - delta * stroke;
+ } else {
+ from = delta * stroke;
+ to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
+ }
+ attr = {};
+ attr["marker-" + se] = "url(#" + markerId + ")";
+ if (to || from) {
+ attr.d = R.getSubpath(attrs.path, from, to);
+ }
+ $(node, attr);
+ o._.arrows[se + "Path"] = pathId;
+ o._.arrows[se + "Marker"] = markerId;
+ o._.arrows[se + "dx"] = delta;
+ o._.arrows[se + "Type"] = type;
+ o._.arrows[se + "String"] = value;
+ } else {
+ if (isEnd) {
+ from = o._.arrows.startdx * stroke || 0;
+ to = R.getTotalLength(attrs.path) - from;
+ } else {
+ from = 0;
+ to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
+ }
+ o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)});
+ delete o._.arrows[se + "Path"];
+ delete o._.arrows[se + "Marker"];
+ delete o._.arrows[se + "dx"];
+ delete o._.arrows[se + "Type"];
+ delete o._.arrows[se + "String"];
+ }
+ for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) {
+ var item = R._g.doc.getElementById(attr);
+ item && item.parentNode.removeChild(item);
+ }
+ }
+ },
+ dasharray = {
+ "": [0],
+ "none": [0],
+ "-": [3, 1],
+ ".": [1, 1],
+ "-.": [3, 1, 1, 1],
+ "-..": [3, 1, 1, 1, 1, 1],
+ ". ": [1, 3],
+ "- ": [4, 3],
+ "--": [8, 3],
+ "- .": [4, 3, 1, 3],
+ "--.": [8, 3, 1, 3],
+ "--..": [8, 3, 1, 3, 1, 3]
+ },
+ addDashes = function (o, value, params) {
+ value = dasharray[Str(value).toLowerCase()];
+ if (value) {
+ var width = o.attrs["stroke-width"] || "1",
+ butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
+ dashes = [],
+ i = value.length;
+ while (i--) {
+ dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
+ }
+ $(o.node, {"stroke-dasharray": dashes.join(",")});
+ }
+ },
+ setFillAndStroke = function (o, params) {
+ var node = o.node,
+ attrs = o.attrs,
+ vis = node.style.visibility;
+ node.style.visibility = "hidden";
+ for (var att in params) {
+ if (params[has](att)) {
+ if (!R._availableAttrs[has](att)) {
+ continue;
+ }
+ var value = params[att];
+ attrs[att] = value;
+ switch (att) {
+ case "blur":
+ o.blur(value);
+ break;
+ case "title":
+ var title = node.getElementsByTagName("title");
+
+ // Use the existing <title>.
+ if (title.length && (title = title[0])) {
+ title.firstChild.nodeValue = value;
+ } else {
+ title = $("title");
+ var val = R._g.doc.createTextNode(value);
+ title.appendChild(val);
+ node.appendChild(title);
+ }
+ break;
+ case "href":
+ case "target":
+ var pn = node.parentNode;
+ if (pn.tagName.toLowerCase() != "a") {
+ var hl = $("a");
+ pn.insertBefore(hl, node);
+ hl.appendChild(node);
+ pn = hl;
+ }
+ if (att == "target") {
+ pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value);
+ } else {
+ pn.setAttributeNS(xlink, att, value);
+ }
+ break;
+ case "cursor":
+ node.style.cursor = value;
+ break;
+ case "transform":
+ o.transform(value);
+ break;
+ case "arrow-start":
+ addArrow(o, value);
+ break;
+ case "arrow-end":
+ addArrow(o, value, 1);
+ break;
+ case "clip-rect":
+ var rect = Str(value).split(separator);
+ if (rect.length == 4) {
+ o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
+ var el = $("clipPath"),
+ rc = $("rect");
+ el.id = R.createUUID();
+ $(rc, {
+ x: rect[0],
+ y: rect[1],
+ width: rect[2],
+ height: rect[3]
+ });
+ el.appendChild(rc);
+ o.paper.defs.appendChild(el);
+ $(node, {"clip-path": "url(#" + el.id + ")"});
+ o.clip = rc;
+ }
+ if (!value) {
+ var path = node.getAttribute("clip-path");
+ if (path) {
+ var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E));
+ clip && clip.parentNode.removeChild(clip);
+ $(node, {"clip-path": E});
+ delete o.clip;
+ }
+ }
+ break;
+ case "path":
+ if (o.type == "path") {
+ $(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"});
+ o._.dirty = 1;
+ if (o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ }
+ break;
+ case "width":
+ node.setAttribute(att, value);
+ o._.dirty = 1;
+ if (attrs.fx) {
+ att = "x";
+ value = attrs.x;
+ } else {
+ break;
+ }
+ case "x":
+ if (attrs.fx) {
+ value = -attrs.x - (attrs.width || 0);
+ }
+ case "rx":
+ if (att == "rx" && o.type == "rect") {
+ break;
+ }
+ case "cx":
+ node.setAttribute(att, value);
+ o.pattern && updatePosition(o);
+ o._.dirty = 1;
+ break;
+ case "height":
+ node.setAttribute(att, value);
+ o._.dirty = 1;
+ if (attrs.fy) {
+ att = "y";
+ value = attrs.y;
+ } else {
+ break;
+ }
+ case "y":
+ if (attrs.fy) {
+ value = -attrs.y - (attrs.height || 0);
+ }
+ case "ry":
+ if (att == "ry" && o.type == "rect") {
+ break;
+ }
+ case "cy":
+ node.setAttribute(att, value);
+ o.pattern && updatePosition(o);
+ o._.dirty = 1;
+ break;
+ case "r":
+ if (o.type == "rect") {
+ $(node, {rx: value, ry: value});
+ } else {
+ node.setAttribute(att, value);
+ }
+ o._.dirty = 1;
+ break;
+ case "src":
+ if (o.type == "image") {
+ node.setAttributeNS(xlink, "href", value);
+ }
+ break;
+ case "stroke-width":
+ if (o._.sx != 1 || o._.sy != 1) {
+ value /= mmax(abs(o._.sx), abs(o._.sy)) || 1;
+ }
+ node.setAttribute(att, value);
+ if (attrs["stroke-dasharray"]) {
+ addDashes(o, attrs["stroke-dasharray"], params);
+ }
+ if (o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ break;
+ case "stroke-dasharray":
+ addDashes(o, value, params);
+ break;
+ case "fill":
+ var isURL = Str(value).match(R._ISURL);
+ if (isURL) {
+ el = $("pattern");
+ var ig = $("image");
+ el.id = R.createUUID();
+ $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
+ $(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
+ el.appendChild(ig);
+
+ (function (el) {
+ R._preload(isURL[1], function () {
+ var w = this.offsetWidth,
+ h = this.offsetHeight;
+ $(el, {width: w, height: h});
+ $(ig, {width: w, height: h});
+ o.paper.safari();
+ });
+ })(el);
+ o.paper.defs.appendChild(el);
+ $(node, {fill: "url(#" + el.id + ")"});
+ o.pattern = el;
+ o.pattern && updatePosition(o);
+ break;
+ }
+ var clr = R.getRGB(value);
+ if (!clr.error) {
+ delete params.gradient;
+ delete attrs.gradient;
+ !R.is(attrs.opacity, "undefined") &&
+ R.is(params.opacity, "undefined") &&
+ $(node, {opacity: attrs.opacity});
+ !R.is(attrs["fill-opacity"], "undefined") &&
+ R.is(params["fill-opacity"], "undefined") &&
+ $(node, {"fill-opacity": attrs["fill-opacity"]});
+ } else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) {
+ if ("opacity" in attrs || "fill-opacity" in attrs) {
+ var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
+ if (gradient) {
+ var stops = gradient.getElementsByTagName("stop");
+ $(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)});
+ }
+ }
+ attrs.gradient = value;
+ attrs.fill = "none";
+ break;
+ }
+ clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
+ case "stroke":
+ clr = R.getRGB(value);
+ node.setAttribute(att, clr.hex);
+ att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
+ if (att == "stroke" && o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ break;
+ case "gradient":
+ (o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value);
+ break;
+ case "opacity":
+ if (attrs.gradient && !attrs[has]("stroke-opacity")) {
+ $(node, {"stroke-opacity": value > 1 ? value / 100 : value});
+ }
+ // fall
+ case "fill-opacity":
+ if (attrs.gradient) {
+ gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
+ if (gradient) {
+ stops = gradient.getElementsByTagName("stop");
+ $(stops[stops.length - 1], {"stop-opacity": value});
+ }
+ break;
+ }
+ default:
+ att == "font-size" && (value = toInt(value, 10) + "px");
+ var cssrule = att.replace(/(\-.)/g, function (w) {
+ return w.substring(1).toUpperCase();
+ });
+ node.style[cssrule] = value;
+ o._.dirty = 1;
+ node.setAttribute(att, value);
+ break;
+ }
+ }
+ }
+
+ tuneText(o, params);
+ node.style.visibility = vis;
+ },
+ leading = 1.2,
+ tuneText = function (el, params) {
+ if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
+ return;
+ }
+ var a = el.attrs,
+ node = el.node,
+ fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
+
+ if (params[has]("text")) {
+ a.text = params.text;
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+ var texts = Str(params.text).split("\n"),
+ tspans = [],
+ tspan;
+ for (var i = 0, ii = texts.length; i < ii; i++) {
+ tspan = $("tspan");
+ i && $(tspan, {dy: fontSize * leading, x: a.x});
+ tspan.appendChild(R._g.doc.createTextNode(texts[i]));
+ node.appendChild(tspan);
+ tspans[i] = tspan;
+ }
+ } else {
+ tspans = node.getElementsByTagName("tspan");
+ for (i = 0, ii = tspans.length; i < ii; i++) if (i) {
+ $(tspans[i], {dy: fontSize * leading, x: a.x});
+ } else {
+ $(tspans[0], {dy: 0});
+ }
+ }
+ $(node, {x: a.x, y: a.y});
+ el._.dirty = 1;
+ var bb = el._getBBox(),
+ dif = a.y - (bb.y + bb.height / 2);
+ dif && R.is(dif, "finite") && $(tspans[0], {dy: dif});
+ },
+ getRealNode = function (node) {
+ if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") {
+ return node.parentNode;
+ } else {
+ return node;
+ }
+ },
+ Element = function (node, svg) {
+ var X = 0,
+ Y = 0;
+ /*\
+ * Element.node
+ [ property (object) ]
+ **
+ * Gives you a reference to the DOM object, so you can assign event handlers or just mess around.
+ **
+ * Note: Don’t mess with it.
+ > Usage
+ | // draw a circle at coordinate 10,10 with radius of 10
+ | var c = paper.circle(10, 10, 10);
+ | c.node.onclick = function () {
+ | c.attr("fill", "red");
+ | };
+ \*/
+ this[0] = this.node = node;
+ /*\
+ * Element.raphael
+ [ property (object) ]
+ **
+ * Internal reference to @Raphael object. In case it is not available.
+ > Usage
+ | Raphael.el.red = function () {
+ | var hsb = this.paper.raphael.rgb2hsb(this.attr("fill"));
+ | hsb.h = 1;
+ | this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex});
+ | }
+ \*/
+ node.raphael = true;
+ /*\
+ * Element.id
+ [ property (number) ]
+ **
+ * Unique id of the element. Especially useful when you want to listen to events of the element,
+ * because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method.
+ \*/
+ this.id = R._oid++;
+ node.raphaelid = this.id;
+ this.matrix = R.matrix();
+ this.realPath = null;
+ /*\
+ * Element.paper
+ [ property (object) ]
+ **
+ * Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions.
+ > Usage
+ | Raphael.el.cross = function () {
+ | this.attr({fill: "red"});
+ | this.paper.path("M10,10L50,50M50,10L10,50")
+ | .attr({stroke: "red"});
+ | }
+ \*/
+ this.paper = svg;
+ this.attrs = this.attrs || {};
+ this._ = {
+ transform: [],
+ sx: 1,
+ sy: 1,
+ deg: 0,
+ dx: 0,
+ dy: 0,
+ dirty: 1
+ };
+ !svg.bottom && (svg.bottom = this);
+ /*\
+ * Element.prev
+ [ property (object) ]
+ **
+ * Reference to the previous element in the hierarchy.
+ \*/
+ this.prev = svg.top;
+ svg.top && (svg.top.next = this);
+ svg.top = this;
+ /*\
+ * Element.next
+ [ property (object) ]
+ **
+ * Reference to the next element in the hierarchy.
+ \*/
+ this.next = null;
+ },
+ elproto = R.el;
+
+ Element.prototype = elproto;
+ elproto.constructor = Element;
+
+ R._engine.path = function (pathString, SVG) {
+ var el = $("path");
+ SVG.canvas && SVG.canvas.appendChild(el);
+ var p = new Element(el, SVG);
+ p.type = "path";
+ setFillAndStroke(p, {
+ fill: "none",
+ stroke: "#000",
+ path: pathString
+ });
+ return p;
+ };
+ /*\
+ * Element.rotate
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds rotation by given angle around given point to the list of
+ * transformations of the element.
+ > Parameters
+ - deg (number) angle in degrees
+ - cx (number) #optional x coordinate of the centre of rotation
+ - cy (number) #optional y coordinate of the centre of rotation
+ * If cx & cy aren’t specified centre of the shape is used as a point of rotation.
+ = (object) @Element
+ \*/
+ elproto.rotate = function (deg, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ deg = Str(deg).split(separator);
+ if (deg.length - 1) {
+ cx = toFloat(deg[1]);
+ cy = toFloat(deg[2]);
+ }
+ deg = toFloat(deg[0]);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ cx = bbox.x + bbox.width / 2;
+ cy = bbox.y + bbox.height / 2;
+ }
+ this.transform(this._.transform.concat([["r", deg, cx, cy]]));
+ return this;
+ };
+ /*\
+ * Element.scale
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds scale by given amount relative to given point to the list of
+ * transformations of the element.
+ > Parameters
+ - sx (number) horisontal scale amount
+ - sy (number) vertical scale amount
+ - cx (number) #optional x coordinate of the centre of scale
+ - cy (number) #optional y coordinate of the centre of scale
+ * If cx & cy aren’t specified centre of the shape is used instead.
+ = (object) @Element
+ \*/
+ elproto.scale = function (sx, sy, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ sx = Str(sx).split(separator);
+ if (sx.length - 1) {
+ sy = toFloat(sx[1]);
+ cx = toFloat(sx[2]);
+ cy = toFloat(sx[3]);
+ }
+ sx = toFloat(sx[0]);
+ (sy == null) && (sy = sx);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ }
+ cx = cx == null ? bbox.x + bbox.width / 2 : cx;
+ cy = cy == null ? bbox.y + bbox.height / 2 : cy;
+ this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
+ return this;
+ };
+ /*\
+ * Element.translate
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds translation by given amount to the list of transformations of the element.
+ > Parameters
+ - dx (number) horisontal shift
+ - dy (number) vertical shift
+ = (object) @Element
+ \*/
+ elproto.translate = function (dx, dy) {
+ if (this.removed) {
+ return this;
+ }
+ dx = Str(dx).split(separator);
+ if (dx.length - 1) {
+ dy = toFloat(dx[1]);
+ }
+ dx = toFloat(dx[0]) || 0;
+ dy = +dy || 0;
+ this.transform(this._.transform.concat([["t", dx, dy]]));
+ return this;
+ };
+ /*\
+ * Element.transform
+ [ method ]
+ **
+ * Adds transformation to the element which is separate to other attributes,
+ * i.e. translation doesn’t change `x` or `y` of the rectange. The format
+ * of transformation string is similar to the path string syntax:
+ | "t100,100r30,100,100s2,2,100,100r45s1.5"
+ * Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for
+ * scale and `m` is for matrix.
+ *
+ * There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`.
+ *
+ * So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100;
+ * rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin
+ * coordinates as optional parameters, the default is the centre point of the element.
+ * Matrix accepts six parameters.
+ > Usage
+ | var el = paper.rect(10, 20, 300, 200);
+ | // translate 100, 100, rotate 45°, translate -100, 0
+ | el.transform("t100,100r45t-100,0");
+ | // if you want you can append or prepend transformations
+ | el.transform("...t50,50");
+ | el.transform("s2...");
+ | // or even wrap
+ | el.transform("t50,50...t-50-50");
+ | // to reset transformation call method with empty string
+ | el.transform("");
+ | // to get current value call it without parameters
+ | console.log(el.transform());
+ > Parameters
+ - tstr (string) #optional transformation string
+ * If tstr isn’t specified
+ = (string) current transformation string
+ * else
+ = (object) @Element
+ \*/
+ elproto.transform = function (tstr) {
+ var _ = this._;
+ if (tstr == null) {
+ return _.transform;
+ }
+ R._extractTransform(this, tstr);
+
+ this.clip && $(this.clip, {transform: this.matrix.invert()});
+ this.pattern && updatePosition(this);
+ this.node && $(this.node, {transform: this.matrix});
+
+ if (_.sx != 1 || _.sy != 1) {
+ var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1;
+ this.attr({"stroke-width": sw});
+ }
+
+ return this;
+ };
+ /*\
+ * Element.hide
+ [ method ]
+ **
+ * Makes element invisible. See @Element.show.
+ = (object) @Element
+ \*/
+ elproto.hide = function () {
+ !this.removed && this.paper.safari(this.node.style.display = "none");
+ return this;
+ };
+ /*\
+ * Element.show
+ [ method ]
+ **
+ * Makes element visible. See @Element.hide.
+ = (object) @Element
+ \*/
+ elproto.show = function () {
+ !this.removed && this.paper.safari(this.node.style.display = "");
+ return this;
+ };
+ /*\
+ * Element.remove
+ [ method ]
+ **
+ * Removes element from the paper.
+ \*/
+ elproto.remove = function () {
+ var node = getRealNode(this.node);
+ if (this.removed || !node.parentNode) {
+ return;
+ }
+ var paper = this.paper;
+ paper.__set__ && paper.__set__.exclude(this);
+ eve.unbind("raphael.*.*." + this.id);
+ if (this.gradient) {
+ paper.defs.removeChild(this.gradient);
+ }
+ R._tear(this, paper);
+
+ node.parentNode.removeChild(node);
+
+ // Remove custom data for element
+ this.removeData();
+
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ this.removed = true;
+ };
+ elproto._getBBox = function () {
+ if (this.node.style.display == "none") {
+ this.show();
+ var hide = true;
+ }
+ var canvasHidden = false,
+ containerStyle;
+ if (this.paper.canvas.parentElement) {
+ containerStyle = this.paper.canvas.parentElement.style;
+ } //IE10+ can't find parentElement
+ else if (this.paper.canvas.parentNode) {
+ containerStyle = this.paper.canvas.parentNode.style;
+ }
+
+ if(containerStyle && containerStyle.display == "none") {
+ canvasHidden = true;
+ containerStyle.display = "";
+ }
+ var bbox = {};
+ try {
+ bbox = this.node.getBBox();
+ } catch(e) {
+ // Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix
+ bbox = {
+ x: this.node.clientLeft,
+ y: this.node.clientTop,
+ width: this.node.clientWidth,
+ height: this.node.clientHeight
+ }
+ } finally {
+ bbox = bbox || {};
+ if(canvasHidden){
+ containerStyle.display = "none";
+ }
+ }
+ hide && this.hide();
+ return bbox;
+ };
+ /*\
+ * Element.attr
+ [ method ]
+ **
+ * Sets the attributes of the element.
+ > Parameters
+ - attrName (string) attribute’s name
+ - value (string) value
+ * or
+ - params (object) object of name/value pairs
+ * or
+ - attrName (string) attribute’s name
+ * or
+ - attrNames (array) in this case method returns array of current values for given attribute names
+ = (object) @Element if attrsName & value or params are passed in.
+ = (...) value of the attribute if only attrsName is passed in.
+ = (array) array of values of the attribute if attrsNames is passed in.
+ = (object) object of attributes if nothing is passed in.
+ > Possible parameters
+ # <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p>
+ o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`.
+ o clip-rect (string) comma or space separated values: x, y, width and height
+ o cursor (string) CSS type of the cursor
+ o cx (number) the x-axis coordinate of the center of the circle, or ellipse
+ o cy (number) the y-axis coordinate of the center of the circle, or ellipse
+ o fill (string) colour, gradient or image
+ o fill-opacity (number)
+ o font (string)
+ o font-family (string)
+ o font-size (number) font size in pixels
+ o font-weight (string)
+ o height (number)
+ o href (string) URL, if specified element behaves as hyperlink
+ o opacity (number)
+ o path (string) SVG path string format
+ o r (number) radius of the circle, ellipse or rounded corner on the rect
+ o rx (number) horisontal radius of the ellipse
+ o ry (number) vertical radius of the ellipse
+ o src (string) image URL, only works for @Element.image element
+ o stroke (string) stroke colour
+ o stroke-dasharray (string) [“”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”]
+ o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”]
+ o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”]
+ o stroke-miterlimit (number)
+ o stroke-opacity (number)
+ o stroke-width (number) stroke width in pixels, default is '1'
+ o target (string) used with href
+ o text (string) contents of the text element. Use `\n` for multiline text
+ o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`”
+ o title (string) will create tooltip with a given text
+ o transform (string) see @Element.transform
+ o width (number)
+ o x (number)
+ o y (number)
+ > Gradients
+ * Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90°
+ * gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black.
+ *
+ * radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” –
+ * gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point
+ * at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses.
+ > Path String
+ # <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p>
+ > Colour Parsing
+ # <ul>
+ # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
+ # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
+ # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
+ # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
+ # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
+ # <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200,&nbsp;100,&nbsp;0, .5)</code>”)</li>
+ # <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%,&nbsp;175%,&nbsp;0%, 50%)</code>”)</li>
+ # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
+ # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li>
+ # <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li>
+ # <li>hsl(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li>
+ # <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg,&nbsp;1,&nbsp;.5)</code>” or, if you want to go fancy, “<code>hsl(240°,&nbsp;1,&nbsp;.5)</code>”</li>
+ # </ul>
+ \*/
+ elproto.attr = function (name, value) {
+ if (this.removed) {
+ return this;
+ }
+ if (name == null) {
+ var res = {};
+ for (var a in this.attrs) if (this.attrs[has](a)) {
+ res[a] = this.attrs[a];
+ }
+ res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+ res.transform = this._.transform;
+ return res;
+ }
+ if (value == null && R.is(name, "string")) {
+ if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
+ return this.attrs.gradient;
+ }
+ if (name == "transform") {
+ return this._.transform;
+ }
+ var names = name.split(separator),
+ out = {};
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ name = names[i];
+ if (name in this.attrs) {
+ out[name] = this.attrs[name];
+ } else if (R.is(this.paper.customAttributes[name], "function")) {
+ out[name] = this.paper.customAttributes[name].def;
+ } else {
+ out[name] = R._availableAttrs[name];
+ }
+ }
+ return ii - 1 ? out : out[names[0]];
+ }
+ if (value == null && R.is(name, "array")) {
+ out = {};
+ for (i = 0, ii = name.length; i < ii; i++) {
+ out[name[i]] = this.attr(name[i]);
+ }
+ return out;
+ }
+ if (value != null) {
+ var params = {};
+ params[name] = value;
+ } else if (name != null && R.is(name, "object")) {
+ params = name;
+ }
+ for (var key in params) {
+ eve("raphael.attr." + key + "." + this.id, this, params[key]);
+ }
+ for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
+ var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
+ this.attrs[key] = params[key];
+ for (var subkey in par) if (par[has](subkey)) {
+ params[subkey] = par[subkey];
+ }
+ }
+ setFillAndStroke(this, params);
+ return this;
+ };
+ /*\
+ * Element.toFront
+ [ method ]
+ **
+ * Moves the element so it is the closest to the viewer’s eyes, on top of other elements.
+ = (object) @Element
+ \*/
+ elproto.toFront = function () {
+ if (this.removed) {
+ return this;
+ }
+ var node = getRealNode(this.node);
+ node.parentNode.appendChild(node);
+ var svg = this.paper;
+ svg.top != this && R._tofront(this, svg);
+ return this;
+ };
+ /*\
+ * Element.toBack
+ [ method ]
+ **
+ * Moves the element so it is the furthest from the viewer’s eyes, behind other elements.
+ = (object) @Element
+ \*/
+ elproto.toBack = function () {
+ if (this.removed) {
+ return this;
+ }
+ var node = getRealNode(this.node);
+ var parentNode = node.parentNode;
+ parentNode.insertBefore(node, parentNode.firstChild);
+ R._toback(this, this.paper);
+ var svg = this.paper;
+ return this;
+ };
+ /*\
+ * Element.insertAfter
+ [ method ]
+ **
+ * Inserts current object after the given one.
+ = (object) @Element
+ \*/
+ elproto.insertAfter = function (element) {
+ if (this.removed || !element) {
+ return this;
+ }
+
+ var node = getRealNode(this.node);
+ var afterNode = getRealNode(element.node || element[element.length - 1].node);
+ if (afterNode.nextSibling) {
+ afterNode.parentNode.insertBefore(node, afterNode.nextSibling);
+ } else {
+ afterNode.parentNode.appendChild(node);
+ }
+ R._insertafter(this, element, this.paper);
+ return this;
+ };
+ /*\
+ * Element.insertBefore
+ [ method ]
+ **
+ * Inserts current object before the given one.
+ = (object) @Element
+ \*/
+ elproto.insertBefore = function (element) {
+ if (this.removed || !element) {
+ return this;
+ }
+
+ var node = getRealNode(this.node);
+ var beforeNode = getRealNode(element.node || element[0].node);
+ beforeNode.parentNode.insertBefore(node, beforeNode);
+ R._insertbefore(this, element, this.paper);
+ return this;
+ };
+ elproto.blur = function (size) {
+ // Experimental. No Safari support. Use it on your own risk.
+ var t = this;
+ if (+size !== 0) {
+ var fltr = $("filter"),
+ blur = $("feGaussianBlur");
+ t.attrs.blur = size;
+ fltr.id = R.createUUID();
+ $(blur, {stdDeviation: +size || 1.5});
+ fltr.appendChild(blur);
+ t.paper.defs.appendChild(fltr);
+ t._blur = fltr;
+ $(t.node, {filter: "url(#" + fltr.id + ")"});
+ } else {
+ if (t._blur) {
+ t._blur.parentNode.removeChild(t._blur);
+ delete t._blur;
+ delete t.attrs.blur;
+ }
+ t.node.removeAttribute("filter");
+ }
+ return t;
+ };
+ R._engine.circle = function (svg, x, y, r) {
+ var el = $("circle");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"};
+ res.type = "circle";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.rect = function (svg, x, y, w, h, r) {
+ var el = $("rect");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"};
+ res.type = "rect";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.ellipse = function (svg, x, y, rx, ry) {
+ var el = $("ellipse");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"};
+ res.type = "ellipse";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.image = function (svg, src, x, y, w, h) {
+ var el = $("image");
+ $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"});
+ el.setAttributeNS(xlink, "href", src);
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {x: x, y: y, width: w, height: h, src: src};
+ res.type = "image";
+ return res;
+ };
+ R._engine.text = function (svg, x, y, text) {
+ var el = $("text");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {
+ x: x,
+ y: y,
+ "text-anchor": "middle",
+ text: text,
+ "font-family": R._availableAttrs["font-family"],
+ "font-size": R._availableAttrs["font-size"],
+ stroke: "none",
+ fill: "#000"
+ };
+ res.type = "text";
+ setFillAndStroke(res, res.attrs);
+ return res;
+ };
+ R._engine.setSize = function (width, height) {
+ this.width = width || this.width;
+ this.height = height || this.height;
+ this.canvas.setAttribute("width", this.width);
+ this.canvas.setAttribute("height", this.height);
+ if (this._viewBox) {
+ this.setViewBox.apply(this, this._viewBox);
+ }
+ return this;
+ };
+ R._engine.create = function () {
+ var con = R._getContainer.apply(0, arguments),
+ container = con && con.container,
+ x = con.x,
+ y = con.y,
+ width = con.width,
+ height = con.height;
+ if (!container) {
+ throw new Error("SVG container not found.");
+ }
+ var cnvs = $("svg"),
+ css = "overflow:hidden;",
+ isFloating;
+ x = x || 0;
+ y = y || 0;
+ width = width || 512;
+ height = height || 342;
+ $(cnvs, {
+ height: height,
+ version: 1.1,
+ width: width,
+ xmlns: "http://www.w3.org/2000/svg",
+ "xmlns:xlink": "http://www.w3.org/1999/xlink"
+ });
+ if (container == 1) {
+ cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px";
+ R._g.doc.body.appendChild(cnvs);
+ isFloating = 1;
+ } else {
+ cnvs.style.cssText = css + "position:relative";
+ if (container.firstChild) {
+ container.insertBefore(cnvs, container.firstChild);
+ } else {
+ container.appendChild(cnvs);
+ }
+ }
+ container = new R._Paper;
+ container.width = width;
+ container.height = height;
+ container.canvas = cnvs;
+ container.clear();
+ container._left = container._top = 0;
+ isFloating && (container.renderfix = function () {});
+ container.renderfix();
+ return container;
+ };
+ R._engine.setViewBox = function (x, y, w, h, fit) {
+ eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
+ var paperSize = this.getSize(),
+ size = mmax(w / paperSize.width, h / paperSize.height),
+ top = this.top,
+ aspectRatio = fit ? "xMidYMid meet" : "xMinYMin",
+ vb,
+ sw;
+ if (x == null) {
+ if (this._vbSize) {
+ size = 1;
+ }
+ delete this._vbSize;
+ vb = "0 0 " + this.width + S + this.height;
+ } else {
+ this._vbSize = size;
+ vb = x + S + y + S + w + S + h;
+ }
+ $(this.canvas, {
+ viewBox: vb,
+ preserveAspectRatio: aspectRatio
+ });
+ while (size && top) {
+ sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1;
+ top.attr({"stroke-width": sw});
+ top._.dirty = 1;
+ top._.dirtyT = 1;
+ top = top.prev;
+ }
+ this._viewBox = [x, y, w, h, !!fit];
+ return this;
+ };
+ /*\
+ * Paper.renderfix
+ [ method ]
+ **
+ * Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant
+ * on other elements after reflow it could shift half pixel which cause for lines to lost their crispness.
+ * This method fixes the issue.
+ **
+ Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method.
+ \*/
+ R.prototype.renderfix = function () {
+ var cnvs = this.canvas,
+ s = cnvs.style,
+ pos;
+ try {
+ pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix();
+ } catch (e) {
+ pos = cnvs.createSVGMatrix();
+ }
+ var left = -pos.e % 1,
+ top = -pos.f % 1;
+ if (left || top) {
+ if (left) {
+ this._left = (this._left + left) % 1;
+ s.left = this._left + "px";
+ }
+ if (top) {
+ this._top = (this._top + top) % 1;
+ s.top = this._top + "px";
+ }
+ }
+ };
+ /*\
+ * Paper.clear
+ [ method ]
+ **
+ * Clears the paper, i.e. removes all the elements.
+ \*/
+ R.prototype.clear = function () {
+ R.eve("raphael.clear", this);
+ var c = this.canvas;
+ while (c.firstChild) {
+ c.removeChild(c.firstChild);
+ }
+ this.bottom = this.top = null;
+ (this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version));
+ c.appendChild(this.desc);
+ c.appendChild(this.defs = $("defs"));
+ };
+ /*\
+ * Paper.remove
+ [ method ]
+ **
+ * Removes the paper from the DOM.
+ \*/
+ R.prototype.remove = function () {
+ eve("raphael.remove", this);
+ this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ };
+ var setproto = R.st;
+ for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname].apply(el, arg);
+ });
+ };
+ })(method);
+ }
+})();
+
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ VML Module │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function(){
+ if (!R.vml) {
+ return;
+ }
+ var has = "hasOwnProperty",
+ Str = String,
+ toFloat = parseFloat,
+ math = Math,
+ round = math.round,
+ mmax = math.max,
+ mmin = math.min,
+ abs = math.abs,
+ fillString = "fill",
+ separator = /[, ]+/,
+ eve = R.eve,
+ ms = " progid:DXImageTransform.Microsoft",
+ S = " ",
+ E = "",
+ map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
+ bites = /([clmz]),?([^clmz]*)/gi,
+ blurregexp = / progid:\S+Blur\([^\)]+\)/g,
+ val = /-?[^,\s-]+/g,
+ cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)",
+ zoom = 21600,
+ pathTypes = {path: 1, rect: 1, image: 1},
+ ovalTypes = {circle: 1, ellipse: 1},
+ path2vml = function (path) {
+ var total = /[ahqstv]/ig,
+ command = R._pathToAbsolute;
+ Str(path).match(total) && (command = R._path2curve);
+ total = /[clmz]/g;
+ if (command == R._pathToAbsolute && !Str(path).match(total)) {
+ var res = Str(path).replace(bites, function (all, command, args) {
+ var vals = [],
+ isMove = command.toLowerCase() == "m",
+ res = map[command];
+ args.replace(val, function (value) {
+ if (isMove && vals.length == 2) {
+ res += vals + map[command == "m" ? "l" : "L"];
+ vals = [];
+ }
+ vals.push(round(value * zoom));
+ });
+ return res + vals;
+ });
+ return res;
+ }
+ var pa = command(path), p, r;
+ res = [];
+ for (var i = 0, ii = pa.length; i < ii; i++) {
+ p = pa[i];
+ r = pa[i][0].toLowerCase();
+ r == "z" && (r = "x");
+ for (var j = 1, jj = p.length; j < jj; j++) {
+ r += round(p[j] * zoom) + (j != jj - 1 ? "," : E);
+ }
+ res.push(r);
+ }
+ return res.join(S);
+ },
+ compensation = function (deg, dx, dy) {
+ var m = R.matrix();
+ m.rotate(-deg, .5, .5);
+ return {
+ dx: m.x(dx, dy),
+ dy: m.y(dx, dy)
+ };
+ },
+ setCoords = function (p, sx, sy, dx, dy, deg) {
+ var _ = p._,
+ m = p.matrix,
+ fillpos = _.fillpos,
+ o = p.node,
+ s = o.style,
+ y = 1,
+ flip = "",
+ dxdy,
+ kx = zoom / sx,
+ ky = zoom / sy;
+ s.visibility = "hidden";
+ if (!sx || !sy) {
+ return;
+ }
+ o.coordsize = abs(kx) + S + abs(ky);
+ s.rotation = deg * (sx * sy < 0 ? -1 : 1);
+ if (deg) {
+ var c = compensation(deg, dx, dy);
+ dx = c.dx;
+ dy = c.dy;
+ }
+ sx < 0 && (flip += "x");
+ sy < 0 && (flip += " y") && (y = -1);
+ s.flip = flip;
+ o.coordorigin = (dx * -kx) + S + (dy * -ky);
+ if (fillpos || _.fillsize) {
+ var fill = o.getElementsByTagName(fillString);
+ fill = fill && fill[0];
+ o.removeChild(fill);
+ if (fillpos) {
+ c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1]));
+ fill.position = c.dx * y + S + c.dy * y;
+ }
+ if (_.fillsize) {
+ fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy);
+ }
+ o.appendChild(fill);
+ }
+ s.visibility = "visible";
+ };
+ R.toString = function () {
+ return "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version;
+ };
+ var addArrow = function (o, value, isEnd) {
+ var values = Str(value).toLowerCase().split("-"),
+ se = isEnd ? "end" : "start",
+ i = values.length,
+ type = "classic",
+ w = "medium",
+ h = "medium";
+ while (i--) {
+ switch (values[i]) {
+ case "block":
+ case "classic":
+ case "oval":
+ case "diamond":
+ case "open":
+ case "none":
+ type = values[i];
+ break;
+ case "wide":
+ case "narrow": h = values[i]; break;
+ case "long":
+ case "short": w = values[i]; break;
+ }
+ }
+ var stroke = o.node.getElementsByTagName("stroke")[0];
+ stroke[se + "arrow"] = type;
+ stroke[se + "arrowlength"] = w;
+ stroke[se + "arrowwidth"] = h;
+ },
+ setFillAndStroke = function (o, params) {
+ // o.paper.canvas.style.display = "none";
+ o.attrs = o.attrs || {};
+ var node = o.node,
+ a = o.attrs,
+ s = node.style,
+ xy,
+ newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r),
+ isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry),
+ res = o;
+
+
+ for (var par in params) if (params[has](par)) {
+ a[par] = params[par];
+ }
+ if (newpath) {
+ a.path = R._getPath[o.type](o);
+ o._.dirty = 1;
+ }
+ params.href && (node.href = params.href);
+ params.title && (node.title = params.title);
+ params.target && (node.target = params.target);
+ params.cursor && (s.cursor = params.cursor);
+ "blur" in params && o.blur(params.blur);
+ if (params.path && o.type == "path" || newpath) {
+ node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path);
+ o._.dirty = 1;
+ if (o.type == "image") {
+ o._.fillpos = [a.x, a.y];
+ o._.fillsize = [a.width, a.height];
+ setCoords(o, 1, 1, 0, 0, 0);
+ }
+ }
+ "transform" in params && o.transform(params.transform);
+ if (isOval) {
+ var cx = +a.cx,
+ cy = +a.cy,
+ rx = +a.rx || +a.r || 0,
+ ry = +a.ry || +a.r || 0;
+ node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom));
+ o._.dirty = 1;
+ }
+ if ("clip-rect" in params) {
+ var rect = Str(params["clip-rect"]).split(separator);
+ if (rect.length == 4) {
+ rect[2] = +rect[2] + (+rect[0]);
+ rect[3] = +rect[3] + (+rect[1]);
+ var div = node.clipRect || R._g.doc.createElement("div"),
+ dstyle = div.style;
+ dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect);
+ if (!node.clipRect) {
+ dstyle.position = "absolute";
+ dstyle.top = 0;
+ dstyle.left = 0;
+ dstyle.width = o.paper.width + "px";
+ dstyle.height = o.paper.height + "px";
+ node.parentNode.insertBefore(div, node);
+ div.appendChild(node);
+ node.clipRect = div;
+ }
+ }
+ if (!params["clip-rect"]) {
+ node.clipRect && (node.clipRect.style.clip = "auto");
+ }
+ }
+ if (o.textpath) {
+ var textpathStyle = o.textpath.style;
+ params.font && (textpathStyle.font = params.font);
+ params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"');
+ params["font-size"] && (textpathStyle.fontSize = params["font-size"]);
+ params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]);
+ params["font-style"] && (textpathStyle.fontStyle = params["font-style"]);
+ }
+ if ("arrow-start" in params) {
+ addArrow(res, params["arrow-start"]);
+ }
+ if ("arrow-end" in params) {
+ addArrow(res, params["arrow-end"], 1);
+ }
+ if (params.opacity != null ||
+ params["stroke-width"] != null ||
+ params.fill != null ||
+ params.src != null ||
+ params.stroke != null ||
+ params["stroke-width"] != null ||
+ params["stroke-opacity"] != null ||
+ params["fill-opacity"] != null ||
+ params["stroke-dasharray"] != null ||
+ params["stroke-miterlimit"] != null ||
+ params["stroke-linejoin"] != null ||
+ params["stroke-linecap"] != null) {
+ var fill = node.getElementsByTagName(fillString),
+ newfill = false;
+ fill = fill && fill[0];
+ !fill && (newfill = fill = createNode(fillString));
+ if (o.type == "image" && params.src) {
+ fill.src = params.src;
+ }
+ params.fill && (fill.on = true);
+ if (fill.on == null || params.fill == "none" || params.fill === null) {
+ fill.on = false;
+ }
+ if (fill.on && params.fill) {
+ var isURL = Str(params.fill).match(R._ISURL);
+ if (isURL) {
+ fill.parentNode == node && node.removeChild(fill);
+ fill.rotate = true;
+ fill.src = isURL[1];
+ fill.type = "tile";
+ var bbox = o.getBBox(1);
+ fill.position = bbox.x + S + bbox.y;
+ o._.fillpos = [bbox.x, bbox.y];
+
+ R._preload(isURL[1], function () {
+ o._.fillsize = [this.offsetWidth, this.offsetHeight];
+ });
+ } else {
+ fill.color = R.getRGB(params.fill).hex;
+ fill.src = E;
+ fill.type = "solid";
+ if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) {
+ a.fill = "none";
+ a.gradient = params.fill;
+ fill.rotate = false;
+ }
+ }
+ }
+ if ("fill-opacity" in params || "opacity" in params) {
+ var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1);
+ opacity = mmin(mmax(opacity, 0), 1);
+ fill.opacity = opacity;
+ if (fill.src) {
+ fill.color = "none";
+ }
+ }
+ node.appendChild(fill);
+ var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]),
+ newstroke = false;
+ !stroke && (newstroke = stroke = createNode("stroke"));
+ if ((params.stroke && params.stroke != "none") ||
+ params["stroke-width"] ||
+ params["stroke-opacity"] != null ||
+ params["stroke-dasharray"] ||
+ params["stroke-miterlimit"] ||
+ params["stroke-linejoin"] ||
+ params["stroke-linecap"]) {
+ stroke.on = true;
+ }
+ (params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false);
+ var strokeColor = R.getRGB(params.stroke);
+ stroke.on && params.stroke && (stroke.color = strokeColor.hex);
+ opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1);
+ var width = (toFloat(params["stroke-width"]) || 1) * .75;
+ opacity = mmin(mmax(opacity, 0), 1);
+ params["stroke-width"] == null && (width = a["stroke-width"]);
+ params["stroke-width"] && (stroke.weight = width);
+ width && width < 1 && (opacity *= width) && (stroke.weight = 1);
+ stroke.opacity = opacity;
+
+ params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
+ stroke.miterlimit = params["stroke-miterlimit"] || 8;
+ params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round");
+ if ("stroke-dasharray" in params) {
+ var dasharray = {
+ "-": "shortdash",
+ ".": "shortdot",
+ "-.": "shortdashdot",
+ "-..": "shortdashdotdot",
+ ". ": "dot",
+ "- ": "dash",
+ "--": "longdash",
+ "- .": "dashdot",
+ "--.": "longdashdot",
+ "--..": "longdashdotdot"
+ };
+ stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E;
+ }
+ newstroke && node.appendChild(stroke);
+ }
+ if (res.type == "text") {
+ res.paper.canvas.style.display = E;
+ var span = res.paper.span,
+ m = 100,
+ fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/);
+ s = span.style;
+ a.font && (s.font = a.font);
+ a["font-family"] && (s.fontFamily = a["font-family"]);
+ a["font-weight"] && (s.fontWeight = a["font-weight"]);
+ a["font-style"] && (s.fontStyle = a["font-style"]);
+ fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10;
+ s.fontSize = fontSize * m + "px";
+ res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "&#60;").replace(/&/g, "&#38;").replace(/\n/g, "<br>"));
+ var brect = span.getBoundingClientRect();
+ res.W = a.w = (brect.right - brect.left) / m;
+ res.H = a.h = (brect.bottom - brect.top) / m;
+ // res.paper.canvas.style.display = "none";
+ res.X = a.x;
+ res.Y = a.y + res.H / 2;
+
+ ("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1));
+ var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"];
+ for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) {
+ res._.dirty = 1;
+ break;
+ }
+
+ // text-anchor emulation
+ switch (a["text-anchor"]) {
+ case "start":
+ res.textpath.style["v-text-align"] = "left";
+ res.bbx = res.W / 2;
+ break;
+ case "end":
+ res.textpath.style["v-text-align"] = "right";
+ res.bbx = -res.W / 2;
+ break;
+ default:
+ res.textpath.style["v-text-align"] = "center";
+ res.bbx = 0;
+ break;
+ }
+ res.textpath.style["v-text-kern"] = true;
+ }
+ // res.paper.canvas.style.display = E;
+ },
+ addGradientFill = function (o, gradient, fill) {
+ o.attrs = o.attrs || {};
+ var attrs = o.attrs,
+ pow = Math.pow,
+ opacity,
+ oindex,
+ type = "linear",
+ fxfy = ".5 .5";
+ o.attrs.gradient = gradient;
+ gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) {
+ type = "radial";
+ if (fx && fy) {
+ fx = toFloat(fx);
+ fy = toFloat(fy);
+ pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5);
+ fxfy = fx + S + fy;
+ }
+ return E;
+ });
+ gradient = gradient.split(/\s*\-\s*/);
+ if (type == "linear") {
+ var angle = gradient.shift();
+ angle = -toFloat(angle);
+ if (isNaN(angle)) {
+ return null;
+ }
+ }
+ var dots = R._parseDots(gradient);
+ if (!dots) {
+ return null;
+ }
+ o = o.shape || o.node;
+ if (dots.length) {
+ o.removeChild(fill);
+ fill.on = true;
+ fill.method = "none";
+ fill.color = dots[0].color;
+ fill.color2 = dots[dots.length - 1].color;
+ var clrs = [];
+ for (var i = 0, ii = dots.length; i < ii; i++) {
+ dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color);
+ }
+ fill.colors = clrs.length ? clrs.join() : "0% " + fill.color;
+ if (type == "radial") {
+ fill.type = "gradientTitle";
+ fill.focus = "100%";
+ fill.focussize = "0 0";
+ fill.focusposition = fxfy;
+ fill.angle = 0;
+ } else {
+ // fill.rotate= true;
+ fill.type = "gradient";
+ fill.angle = (270 - angle) % 360;
+ }
+ o.appendChild(fill);
+ }
+ return 1;
+ },
+ Element = function (node, vml) {
+ this[0] = this.node = node;
+ node.raphael = true;
+ this.id = R._oid++;
+ node.raphaelid = this.id;
+ this.X = 0;
+ this.Y = 0;
+ this.attrs = {};
+ this.paper = vml;
+ this.matrix = R.matrix();
+ this._ = {
+ transform: [],
+ sx: 1,
+ sy: 1,
+ dx: 0,
+ dy: 0,
+ deg: 0,
+ dirty: 1,
+ dirtyT: 1
+ };
+ !vml.bottom && (vml.bottom = this);
+ this.prev = vml.top;
+ vml.top && (vml.top.next = this);
+ vml.top = this;
+ this.next = null;
+ };
+ var elproto = R.el;
+
+ Element.prototype = elproto;
+ elproto.constructor = Element;
+ elproto.transform = function (tstr) {
+ if (tstr == null) {
+ return this._.transform;
+ }
+ var vbs = this.paper._viewBoxShift,
+ vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E,
+ oldt;
+ if (vbs) {
+ oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E);
+ }
+ R._extractTransform(this, vbt + tstr);
+ var matrix = this.matrix.clone(),
+ skew = this.skew,
+ o = this.node,
+ split,
+ isGrad = ~Str(this.attrs.fill).indexOf("-"),
+ isPatt = !Str(this.attrs.fill).indexOf("url(");
+ matrix.translate(1, 1);
+ if (isPatt || isGrad || this.type == "image") {
+ skew.matrix = "1 0 0 1";
+ skew.offset = "0 0";
+ split = matrix.split();
+ if ((isGrad && split.noRotation) || !split.isSimple) {
+ o.style.filter = matrix.toFilter();
+ var bb = this.getBBox(),
+ bbt = this.getBBox(1),
+ dx = bb.x - bbt.x,
+ dy = bb.y - bbt.y;
+ o.coordorigin = (dx * -zoom) + S + (dy * -zoom);
+ setCoords(this, 1, 1, dx, dy, 0);
+ } else {
+ o.style.filter = E;
+ setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate);
+ }
+ } else {
+ o.style.filter = E;
+ skew.matrix = Str(matrix);
+ skew.offset = matrix.offset();
+ }
+ if (oldt !== null) { // empty string value is true as well
+ this._.transform = oldt;
+ R._extractTransform(this, oldt);
+ }
+ return this;
+ };
+ elproto.rotate = function (deg, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ if (deg == null) {
+ return;
+ }
+ deg = Str(deg).split(separator);
+ if (deg.length - 1) {
+ cx = toFloat(deg[1]);
+ cy = toFloat(deg[2]);
+ }
+ deg = toFloat(deg[0]);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ cx = bbox.x + bbox.width / 2;
+ cy = bbox.y + bbox.height / 2;
+ }
+ this._.dirtyT = 1;
+ this.transform(this._.transform.concat([["r", deg, cx, cy]]));
+ return this;
+ };
+ elproto.translate = function (dx, dy) {
+ if (this.removed) {
+ return this;
+ }
+ dx = Str(dx).split(separator);
+ if (dx.length - 1) {
+ dy = toFloat(dx[1]);
+ }
+ dx = toFloat(dx[0]) || 0;
+ dy = +dy || 0;
+ if (this._.bbox) {
+ this._.bbox.x += dx;
+ this._.bbox.y += dy;
+ }
+ this.transform(this._.transform.concat([["t", dx, dy]]));
+ return this;
+ };
+ elproto.scale = function (sx, sy, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ sx = Str(sx).split(separator);
+ if (sx.length - 1) {
+ sy = toFloat(sx[1]);
+ cx = toFloat(sx[2]);
+ cy = toFloat(sx[3]);
+ isNaN(cx) && (cx = null);
+ isNaN(cy) && (cy = null);
+ }
+ sx = toFloat(sx[0]);
+ (sy == null) && (sy = sx);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ }
+ cx = cx == null ? bbox.x + bbox.width / 2 : cx;
+ cy = cy == null ? bbox.y + bbox.height / 2 : cy;
+
+ this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
+ this._.dirtyT = 1;
+ return this;
+ };
+ elproto.hide = function () {
+ !this.removed && (this.node.style.display = "none");
+ return this;
+ };
+ elproto.show = function () {
+ !this.removed && (this.node.style.display = E);
+ return this;
+ };
+ // Needed to fix the vml setViewBox issues
+ elproto.auxGetBBox = R.el.getBBox;
+ elproto.getBBox = function(){
+ var b = this.auxGetBBox();
+ if (this.paper && this.paper._viewBoxShift)
+ {
+ var c = {};
+ var z = 1/this.paper._viewBoxShift.scale;
+ c.x = b.x - this.paper._viewBoxShift.dx;
+ c.x *= z;
+ c.y = b.y - this.paper._viewBoxShift.dy;
+ c.y *= z;
+ c.width = b.width * z;
+ c.height = b.height * z;
+ c.x2 = c.x + c.width;
+ c.y2 = c.y + c.height;
+ return c;
+ }
+ return b;
+ };
+ elproto._getBBox = function () {
+ if (this.removed) {
+ return {};
+ }
+ return {
+ x: this.X + (this.bbx || 0) - this.W / 2,
+ y: this.Y - this.H,
+ width: this.W,
+ height: this.H
+ };
+ };
+ elproto.remove = function () {
+ if (this.removed || !this.node.parentNode) {
+ return;
+ }
+ this.paper.__set__ && this.paper.__set__.exclude(this);
+ R.eve.unbind("raphael.*.*." + this.id);
+ R._tear(this, this.paper);
+ this.node.parentNode.removeChild(this.node);
+ this.shape && this.shape.parentNode.removeChild(this.shape);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ this.removed = true;
+ };
+ elproto.attr = function (name, value) {
+ if (this.removed) {
+ return this;
+ }
+ if (name == null) {
+ var res = {};
+ for (var a in this.attrs) if (this.attrs[has](a)) {
+ res[a] = this.attrs[a];
+ }
+ res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+ res.transform = this._.transform;
+ return res;
+ }
+ if (value == null && R.is(name, "string")) {
+ if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) {
+ return this.attrs.gradient;
+ }
+ var names = name.split(separator),
+ out = {};
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ name = names[i];
+ if (name in this.attrs) {
+ out[name] = this.attrs[name];
+ } else if (R.is(this.paper.customAttributes[name], "function")) {
+ out[name] = this.paper.customAttributes[name].def;
+ } else {
+ out[name] = R._availableAttrs[name];
+ }
+ }
+ return ii - 1 ? out : out[names[0]];
+ }
+ if (this.attrs && value == null && R.is(name, "array")) {
+ out = {};
+ for (i = 0, ii = name.length; i < ii; i++) {
+ out[name[i]] = this.attr(name[i]);
+ }
+ return out;
+ }
+ var params;
+ if (value != null) {
+ params = {};
+ params[name] = value;
+ }
+ value == null && R.is(name, "object") && (params = name);
+ for (var key in params) {
+ eve("raphael.attr." + key + "." + this.id, this, params[key]);
+ }
+ if (params) {
+ for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
+ var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
+ this.attrs[key] = params[key];
+ for (var subkey in par) if (par[has](subkey)) {
+ params[subkey] = par[subkey];
+ }
+ }
+ // this.paper.canvas.style.display = "none";
+ if (params.text && this.type == "text") {
+ this.textpath.string = params.text;
+ }
+ setFillAndStroke(this, params);
+ // this.paper.canvas.style.display = E;
+ }
+ return this;
+ };
+ elproto.toFront = function () {
+ !this.removed && this.node.parentNode.appendChild(this.node);
+ this.paper && this.paper.top != this && R._tofront(this, this.paper);
+ return this;
+ };
+ elproto.toBack = function () {
+ if (this.removed) {
+ return this;
+ }
+ if (this.node.parentNode.firstChild != this.node) {
+ this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
+ R._toback(this, this.paper);
+ }
+ return this;
+ };
+ elproto.insertAfter = function (element) {
+ if (this.removed) {
+ return this;
+ }
+ if (element.constructor == R.st.constructor) {
+ element = element[element.length - 1];
+ }
+ if (element.node.nextSibling) {
+ element.node.parentNode.insertBefore(this.node, element.node.nextSibling);
+ } else {
+ element.node.parentNode.appendChild(this.node);
+ }
+ R._insertafter(this, element, this.paper);
+ return this;
+ };
+ elproto.insertBefore = function (element) {
+ if (this.removed) {
+ return this;
+ }
+ if (element.constructor == R.st.constructor) {
+ element = element[0];
+ }
+ element.node.parentNode.insertBefore(this.node, element.node);
+ R._insertbefore(this, element, this.paper);
+ return this;
+ };
+ elproto.blur = function (size) {
+ var s = this.node.runtimeStyle,
+ f = s.filter;
+ f = f.replace(blurregexp, E);
+ if (+size !== 0) {
+ this.attrs.blur = size;
+ s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")";
+ s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5));
+ } else {
+ s.filter = f;
+ s.margin = 0;
+ delete this.attrs.blur;
+ }
+ return this;
+ };
+
+ R._engine.path = function (pathString, vml) {
+ var el = createNode("shape");
+ el.style.cssText = cssDot;
+ el.coordsize = zoom + S + zoom;
+ el.coordorigin = vml.coordorigin;
+ var p = new Element(el, vml),
+ attr = {fill: "none", stroke: "#000"};
+ pathString && (attr.path = pathString);
+ p.type = "path";
+ p.path = [];
+ p.Path = E;
+ setFillAndStroke(p, attr);
+ vml.canvas.appendChild(el);
+ var skew = createNode("skew");
+ skew.on = true;
+ el.appendChild(skew);
+ p.skew = skew;
+ p.transform(E);
+ return p;
+ };
+ R._engine.rect = function (vml, x, y, w, h, r) {
+ var path = R._rectPath(x, y, w, h, r),
+ res = vml.path(path),
+ a = res.attrs;
+ res.X = a.x = x;
+ res.Y = a.y = y;
+ res.W = a.width = w;
+ res.H = a.height = h;
+ a.r = r;
+ a.path = path;
+ res.type = "rect";
+ return res;
+ };
+ R._engine.ellipse = function (vml, x, y, rx, ry) {
+ var res = vml.path(),
+ a = res.attrs;
+ res.X = x - rx;
+ res.Y = y - ry;
+ res.W = rx * 2;
+ res.H = ry * 2;
+ res.type = "ellipse";
+ setFillAndStroke(res, {
+ cx: x,
+ cy: y,
+ rx: rx,
+ ry: ry
+ });
+ return res;
+ };
+ R._engine.circle = function (vml, x, y, r) {
+ var res = vml.path(),
+ a = res.attrs;
+ res.X = x - r;
+ res.Y = y - r;
+ res.W = res.H = r * 2;
+ res.type = "circle";
+ setFillAndStroke(res, {
+ cx: x,
+ cy: y,
+ r: r
+ });
+ return res;
+ };
+ R._engine.image = function (vml, src, x, y, w, h) {
+ var path = R._rectPath(x, y, w, h),
+ res = vml.path(path).attr({stroke: "none"}),
+ a = res.attrs,
+ node = res.node,
+ fill = node.getElementsByTagName(fillString)[0];
+ a.src = src;
+ res.X = a.x = x;
+ res.Y = a.y = y;
+ res.W = a.width = w;
+ res.H = a.height = h;
+ a.path = path;
+ res.type = "image";
+ fill.parentNode == node && node.removeChild(fill);
+ fill.rotate = true;
+ fill.src = src;
+ fill.type = "tile";
+ res._.fillpos = [x, y];
+ res._.fillsize = [w, h];
+ node.appendChild(fill);
+ setCoords(res, 1, 1, 0, 0, 0);
+ return res;
+ };
+ R._engine.text = function (vml, x, y, text) {
+ var el = createNode("shape"),
+ path = createNode("path"),
+ o = createNode("textpath");
+ x = x || 0;
+ y = y || 0;
+ text = text || "";
+ path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1);
+ path.textpathok = true;
+ o.string = Str(text);
+ o.on = true;
+ el.style.cssText = cssDot;
+ el.coordsize = zoom + S + zoom;
+ el.coordorigin = "0 0";
+ var p = new Element(el, vml),
+ attr = {
+ fill: "#000",
+ stroke: "none",
+ font: R._availableAttrs.font,
+ text: text
+ };
+ p.shape = el;
+ p.path = path;
+ p.textpath = o;
+ p.type = "text";
+ p.attrs.text = Str(text);
+ p.attrs.x = x;
+ p.attrs.y = y;
+ p.attrs.w = 1;
+ p.attrs.h = 1;
+ setFillAndStroke(p, attr);
+ el.appendChild(o);
+ el.appendChild(path);
+ vml.canvas.appendChild(el);
+ var skew = createNode("skew");
+ skew.on = true;
+ el.appendChild(skew);
+ p.skew = skew;
+ p.transform(E);
+ return p;
+ };
+ R._engine.setSize = function (width, height) {
+ var cs = this.canvas.style;
+ this.width = width;
+ this.height = height;
+ width == +width && (width += "px");
+ height == +height && (height += "px");
+ cs.width = width;
+ cs.height = height;
+ cs.clip = "rect(0 " + width + " " + height + " 0)";
+ if (this._viewBox) {
+ R._engine.setViewBox.apply(this, this._viewBox);
+ }
+ return this;
+ };
+ R._engine.setViewBox = function (x, y, w, h, fit) {
+ R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
+ var paperSize = this.getSize(),
+ width = paperSize.width,
+ height = paperSize.height,
+ H, W;
+ if (fit) {
+ H = height / h;
+ W = width / w;
+ if (w * H < width) {
+ x -= (width - w * H) / 2 / H;
+ }
+ if (h * W < height) {
+ y -= (height - h * W) / 2 / W;
+ }
+ }
+ this._viewBox = [x, y, w, h, !!fit];
+ this._viewBoxShift = {
+ dx: -x,
+ dy: -y,
+ scale: paperSize
+ };
+ this.forEach(function (el) {
+ el.transform("...");
+ });
+ return this;
+ };
+ var createNode;
+ R._engine.initWin = function (win) {
+ var doc = win.document;
+ if (doc.styleSheets.length < 31) {
+ doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
+ } else {
+ // no more room, add to the existing one
+ // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx
+ doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)");
+ }
+ try {
+ !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
+ createNode = function (tagName) {
+ return doc.createElement('<rvml:' + tagName + ' class="rvml">');
+ };
+ } catch (e) {
+ createNode = function (tagName) {
+ return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
+ };
+ }
+ };
+ R._engine.initWin(R._g.win);
+ R._engine.create = function () {
+ var con = R._getContainer.apply(0, arguments),
+ container = con.container,
+ height = con.height,
+ s,
+ width = con.width,
+ x = con.x,
+ y = con.y;
+ if (!container) {
+ throw new Error("VML container not found.");
+ }
+ var res = new R._Paper,
+ c = res.canvas = R._g.doc.createElement("div"),
+ cs = c.style;
+ x = x || 0;
+ y = y || 0;
+ width = width || 512;
+ height = height || 342;
+ res.width = width;
+ res.height = height;
+ width == +width && (width += "px");
+ height == +height && (height += "px");
+ res.coordsize = zoom * 1e3 + S + zoom * 1e3;
+ res.coordorigin = "0 0";
+ res.span = R._g.doc.createElement("span");
+ res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;";
+ c.appendChild(res.span);
+ cs.cssText = R.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden", width, height);
+ if (container == 1) {
+ R._g.doc.body.appendChild(c);
+ cs.left = x + "px";
+ cs.top = y + "px";
+ cs.position = "absolute";
+ } else {
+ if (container.firstChild) {
+ container.insertBefore(c, container.firstChild);
+ } else {
+ container.appendChild(c);
+ }
+ }
+ res.renderfix = function () {};
+ return res;
+ };
+ R.prototype.clear = function () {
+ R.eve("raphael.clear", this);
+ this.canvas.innerHTML = E;
+ this.span = R._g.doc.createElement("span");
+ this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
+ this.canvas.appendChild(this.span);
+ this.bottom = this.top = null;
+ };
+ R.prototype.remove = function () {
+ R.eve("raphael.remove", this);
+ this.canvas.parentNode.removeChild(this.canvas);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ return true;
+ };
+
+ var setproto = R.st;
+ for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname].apply(el, arg);
+ });
+ };
+ })(method);
+ }
+})();
+
+ // EXPOSE
+ // SVG and VML are appended just before the EXPOSE line
+ // Even with AMD, Raphael should be defined globally
+ oldRaphael.was ? (g.win.Raphael = R) : (Raphael = R);
+
+ if(typeof exports == "object"){
+ module.exports = R;
+ }
+ return R;
+}));
diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee
new file mode 100644
index 00000000000..584751af8ea
--- /dev/null
+++ b/vendor/assets/javascripts/task_list.js.coffee
@@ -0,0 +1,258 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2014 GitHub, Inc.
+#
+# 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.
+
+# TaskList Behavior
+#
+#= provides tasklist:enabled
+#= provides tasklist:disabled
+#= provides tasklist:change
+#= provides tasklist:changed
+#
+#
+# Enables Task List update behavior.
+#
+# ### Example Markup
+#
+# <div class="js-task-list-container">
+# <ul class="task-list">
+# <li class="task-list-item">
+# <input type="checkbox" class="js-task-list-item-checkbox" disabled />
+# text
+# </li>
+# </ul>
+# <form>
+# <textarea class="js-task-list-field">- [ ] text</textarea>
+# </form>
+# </div>
+#
+# ### Specification
+#
+# TaskLists MUST be contained in a `(div).js-task-list-container`.
+#
+# TaskList Items SHOULD be an a list (`UL`/`OL`) element.
+#
+# Task list items MUST match `(input).task-list-item-checkbox` and MUST be
+# `disabled` by default.
+#
+# TaskLists MUST have a `(textarea).js-task-list-field` form element whose
+# `value` attribute is the source (Markdown) to be udpated. The source MUST
+# follow the syntax guidelines.
+#
+# TaskList updates trigger `tasklist:change` events. If the change is
+# successful, `tasklist:changed` is fired. The change can be canceled.
+#
+# jQuery is required.
+#
+# ### Methods
+#
+# `.taskList('enable')` or `.taskList()`
+#
+# Enables TaskList updates for the container.
+#
+# `.taskList('disable')`
+#
+# Disables TaskList updates for the container.
+#
+## ### Events
+#
+# `tasklist:enabled`
+#
+# Fired when the TaskList is enabled.
+#
+# * **Synchronicity** Sync
+# * **Bubbles** Yes
+# * **Cancelable** No
+# * **Target** `.js-task-list-container`
+#
+# `tasklist:disabled`
+#
+# Fired when the TaskList is disabled.
+#
+# * **Synchronicity** Sync
+# * **Bubbles** Yes
+# * **Cancelable** No
+# * **Target** `.js-task-list-container`
+#
+# `tasklist:change`
+#
+# Fired before the TaskList item change takes affect.
+#
+# * **Synchronicity** Sync
+# * **Bubbles** Yes
+# * **Cancelable** Yes
+# * **Target** `.js-task-list-field`
+#
+# `tasklist:changed`
+#
+# Fired once the TaskList item change has taken affect.
+#
+# * **Synchronicity** Sync
+# * **Bubbles** Yes
+# * **Cancelable** No
+# * **Target** `.js-task-list-field`
+#
+# ### NOTE
+#
+# Task list checkboxes are rendered as disabled by default because rendered
+# user content is cached without regard for the viewer.
+
+incomplete = "[ ]"
+complete = "[x]"
+
+# Escapes the String for regular expression matching.
+escapePattern = (str) ->
+ str.
+ replace(/([\[\]])/g, "\\$1"). # escape square brackets
+ replace(/\s/, "\\s"). # match all white space
+ replace("x", "[xX]") # match all cases
+
+incompletePattern = ///
+ #{escapePattern(incomplete)}
+///
+completePattern = ///
+ #{escapePattern(complete)}
+///
+
+# Pattern used to identify all task list items.
+# Useful when you need iterate over all items.
+itemPattern = ///
+ ^
+ (?: # prefix, consisting of
+ \s* # optional leading whitespace
+ (?:>\s*)* # zero or more blockquotes
+ (?:[-+*]|(?:\d+\.)) # list item indicator
+ )
+ \s* # optional whitespace prefix
+ ( # checkbox
+ #{escapePattern(complete)}|
+ #{escapePattern(incomplete)}
+ )
+ \s+ # is followed by whitespace
+ (?!
+ \(.*?\) # is not part of a [foo](url) link
+ )
+ (?= # and is followed by zero or more links
+ (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)*
+ (?:[^\[]|$) # and either a non-link or the end of the string
+ )
+///
+
+# Used to filter out code fences from the source for comparison only.
+# http://rubular.com/r/x5EwZVrloI
+# Modified slightly due to issues with JS
+codeFencesPattern = ///
+ ^`{3} # ```
+ (?:\s*\w+)? # followed by optional language
+ [\S\s] # whitespace
+ .* # code
+ [\S\s] # whitespace
+ ^`{3}$ # ```
+///mg
+
+# Used to filter out potential mismatches (items not in lists).
+# http://rubular.com/r/OInl6CiePy
+itemsInParasPattern = ///
+ ^
+ (
+ #{escapePattern(complete)}|
+ #{escapePattern(incomplete)}
+ )
+ .+
+ $
+///g
+
+# Given the source text, updates the appropriate task list item to match the
+# given checked value.
+#
+# Returns the updated String text.
+updateTaskListItem = (source, itemIndex, checked) ->
+ clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').
+ replace(itemsInParasPattern, '').split("\n")
+ index = 0
+ result = for line in source.split("\n")
+ if line in clean && line.match(itemPattern)
+ index += 1
+ if index == itemIndex
+ line =
+ if checked
+ line.replace(incompletePattern, complete)
+ else
+ line.replace(completePattern, incomplete)
+ line
+ result.join("\n")
+
+# Updates the $field value to reflect the state of $item.
+# Triggers the `tasklist:change` event before the value has changed, and fires
+# a `tasklist:changed` event once the value has changed.
+updateTaskList = ($item) ->
+ $container = $item.closest '.js-task-list-container'
+ $field = $container.find '.js-task-list-field'
+ index = 1 + $container.find('.task-list-item-checkbox').index($item)
+ checked = $item.prop 'checked'
+
+ event = $.Event 'tasklist:change'
+ $field.trigger event, [index, checked]
+
+ unless event.isDefaultPrevented()
+ $field.val updateTaskListItem($field.val(), index, checked)
+ $field.trigger 'change'
+ $field.trigger 'tasklist:changed', [index, checked]
+
+# When the task list item checkbox is updated, submit the change
+$(document).on 'change', '.task-list-item-checkbox', ->
+ updateTaskList $(this)
+
+# Enables TaskList item changes.
+enableTaskList = ($container) ->
+ if $container.find('.js-task-list-field').length > 0
+ $container.
+ find('.task-list-item').addClass('enabled').
+ find('.task-list-item-checkbox').attr('disabled', null)
+ $container.addClass('is-task-list-enabled').
+ trigger 'tasklist:enabled'
+
+# Enables a collection of TaskList containers.
+enableTaskLists = ($containers) ->
+ for container in $containers
+ enableTaskList $(container)
+
+# Disable TaskList item changes.
+disableTaskList = ($container) ->
+ $container.
+ find('.task-list-item').removeClass('enabled').
+ find('.task-list-item-checkbox').attr('disabled', 'disabled')
+ $container.removeClass('is-task-list-enabled').
+ trigger 'tasklist:disabled'
+
+# Disables a collection of TaskList containers.
+disableTaskLists = ($containers) ->
+ for container in $containers
+ disableTaskList $(container)
+
+$.fn.taskList = (method) ->
+ $container = $(this).closest('.js-task-list-container')
+
+ methods =
+ enable: enableTaskLists
+ disable: disableTaskLists
+
+ methods[method || 'enable']($container)
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
new file mode 100644
index 00000000000..e666b136051
--- /dev/null
+++ b/vendor/assets/javascripts/u2f.js
@@ -0,0 +1,748 @@
+//Copyright 2014-2015 Google Inc. All rights reserved.
+
+//Use of this source code is governed by a BSD-style
+//license that can be found in the LICENSE file or at
+//https://developers.google.com/open-source/licenses/bsd
+
+/**
+ * @fileoverview The U2F api.
+ */
+'use strict';
+
+
+/**
+ * Namespace for the U2F api.
+ * @type {Object}
+ */
+var u2f = u2f || {};
+
+/**
+ * FIDO U2F Javascript API Version
+ * @number
+ */
+var js_api_version;
+
+/**
+ * The U2F extension id
+ * @const {string}
+ */
+// The Chrome packaged app extension ID.
+// Uncomment this if you want to deploy a server instance that uses
+// the package Chrome app and does not require installing the U2F Chrome extension.
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+// The U2F Chrome extension ID.
+// Uncomment this if you want to deploy a server instance that uses
+// the U2F Chrome extension to authenticate.
+// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
+
+
+/**
+ * Message types for messsages to/from the extension
+ * @const
+ * @enum {string}
+ */
+u2f.MessageTypes = {
+ 'U2F_REGISTER_REQUEST': 'u2f_register_request',
+ 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
+ 'U2F_SIGN_REQUEST': 'u2f_sign_request',
+ 'U2F_SIGN_RESPONSE': 'u2f_sign_response',
+ 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
+ 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
+};
+
+
+/**
+ * Response status codes
+ * @const
+ * @enum {number}
+ */
+u2f.ErrorCodes = {
+ 'OK': 0,
+ 'OTHER_ERROR': 1,
+ 'BAD_REQUEST': 2,
+ 'CONFIGURATION_UNSUPPORTED': 3,
+ 'DEVICE_INELIGIBLE': 4,
+ 'TIMEOUT': 5
+};
+
+
+/**
+ * A message for registration requests
+ * @typedef {{
+ * type: u2f.MessageTypes,
+ * appId: ?string,
+ * timeoutSeconds: ?number,
+ * requestId: ?number
+ * }}
+ */
+u2f.U2fRequest;
+
+
+/**
+ * A message for registration responses
+ * @typedef {{
+ * type: u2f.MessageTypes,
+ * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
+ * requestId: ?number
+ * }}
+ */
+u2f.U2fResponse;
+
+
+/**
+ * An error object for responses
+ * @typedef {{
+ * errorCode: u2f.ErrorCodes,
+ * errorMessage: ?string
+ * }}
+ */
+u2f.Error;
+
+/**
+ * Data object for a single sign request.
+ * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
+ */
+u2f.Transport;
+
+
+/**
+ * Data object for a single sign request.
+ * @typedef {Array<u2f.Transport>}
+ */
+u2f.Transports;
+
+/**
+ * Data object for a single sign request.
+ * @typedef {{
+ * version: string,
+ * challenge: string,
+ * keyHandle: string,
+ * appId: string
+ * }}
+ */
+u2f.SignRequest;
+
+
+/**
+ * Data object for a sign response.
+ * @typedef {{
+ * keyHandle: string,
+ * signatureData: string,
+ * clientData: string
+ * }}
+ */
+u2f.SignResponse;
+
+
+/**
+ * Data object for a registration request.
+ * @typedef {{
+ * version: string,
+ * challenge: string
+ * }}
+ */
+u2f.RegisterRequest;
+
+
+/**
+ * Data object for a registration response.
+ * @typedef {{
+ * version: string,
+ * keyHandle: string,
+ * transports: Transports,
+ * appId: string
+ * }}
+ */
+u2f.RegisterResponse;
+
+
+/**
+ * Data object for a registered key.
+ * @typedef {{
+ * version: string,
+ * keyHandle: string,
+ * transports: ?Transports,
+ * appId: ?string
+ * }}
+ */
+u2f.RegisteredKey;
+
+
+/**
+ * Data object for a get API register response.
+ * @typedef {{
+ * js_api_version: number
+ * }}
+ */
+u2f.GetJsApiVersionResponse;
+
+
+//Low level MessagePort API support
+
+/**
+ * Sets up a MessagePort to the U2F extension using the
+ * available mechanisms.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ */
+u2f.getMessagePort = function(callback) {
+ if (typeof chrome != 'undefined' && chrome.runtime) {
+ // The actual message here does not matter, but we need to get a reply
+ // for the callback to run. Thus, send an empty signature request
+ // in order to get a failure response.
+ var msg = {
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+ signRequests: []
+ };
+ chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
+ if (!chrome.runtime.lastError) {
+ // We are on a whitelisted origin and can talk directly
+ // with the extension.
+ u2f.getChromeRuntimePort_(callback);
+ } else {
+ // chrome.runtime was available, but we couldn't message
+ // the extension directly, use iframe
+ u2f.getIframePort_(callback);
+ }
+ });
+ } else if (u2f.isAndroidChrome_()) {
+ u2f.getAuthenticatorPort_(callback);
+ } else if (u2f.isIosChrome_()) {
+ u2f.getIosPort_(callback);
+ } else {
+ // chrome.runtime was not available at all, which is normal
+ // when this origin doesn't have access to any extensions.
+ u2f.getIframePort_(callback);
+ }
+};
+
+/**
+ * Detect chrome running on android based on the browser's useragent.
+ * @private
+ */
+u2f.isAndroidChrome_ = function() {
+ var userAgent = navigator.userAgent;
+ return userAgent.indexOf('Chrome') != -1 &&
+ userAgent.indexOf('Android') != -1;
+};
+
+/**
+ * Detect chrome running on iOS based on the browser's platform.
+ * @private
+ */
+u2f.isIosChrome_ = function() {
+ return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
+};
+
+/**
+ * Connects directly to the extension via chrome.runtime.connect.
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
+ * @private
+ */
+u2f.getChromeRuntimePort_ = function(callback) {
+ var port = chrome.runtime.connect(u2f.EXTENSION_ID,
+ {'includeTlsChannelId': true});
+ setTimeout(function() {
+ callback(new u2f.WrappedChromeRuntimePort_(port));
+ }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the Authenticator app.
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
+ * @private
+ */
+u2f.getAuthenticatorPort_ = function(callback) {
+ setTimeout(function() {
+ callback(new u2f.WrappedAuthenticatorPort_());
+ }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the iOS client app.
+ * @param {function(u2f.WrappedIosPort_)} callback
+ * @private
+ */
+u2f.getIosPort_ = function(callback) {
+ setTimeout(function() {
+ callback(new u2f.WrappedIosPort_());
+ }, 0);
+};
+
+/**
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
+ * @param {Port} port
+ * @constructor
+ * @private
+ */
+u2f.WrappedChromeRuntimePort_ = function(port) {
+ this.port_ = port;
+};
+
+/**
+ * Format and return a sign request compliant with the JS API version supported by the extension.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.formatSignRequest_ =
+ function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
+ if (js_api_version === undefined || js_api_version < 1.1) {
+ // Adapt request to the 1.0 JS API
+ var signRequests = [];
+ for (var i = 0; i < registeredKeys.length; i++) {
+ signRequests[i] = {
+ version: registeredKeys[i].version,
+ challenge: challenge,
+ keyHandle: registeredKeys[i].keyHandle,
+ appId: appId
+ };
+ }
+ return {
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+ signRequests: signRequests,
+ timeoutSeconds: timeoutSeconds,
+ requestId: reqId
+ };
+ }
+ // JS 1.1 API
+ return {
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+ appId: appId,
+ challenge: challenge,
+ registeredKeys: registeredKeys,
+ timeoutSeconds: timeoutSeconds,
+ requestId: reqId
+ };
+ };
+
+/**
+ * Format and return a register request compliant with the JS API version supported by the extension..
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.formatRegisterRequest_ =
+ function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
+ if (js_api_version === undefined || js_api_version < 1.1) {
+ // Adapt request to the 1.0 JS API
+ for (var i = 0; i < registerRequests.length; i++) {
+ registerRequests[i].appId = appId;
+ }
+ var signRequests = [];
+ for (var i = 0; i < registeredKeys.length; i++) {
+ signRequests[i] = {
+ version: registeredKeys[i].version,
+ challenge: registerRequests[0],
+ keyHandle: registeredKeys[i].keyHandle,
+ appId: appId
+ };
+ }
+ return {
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+ signRequests: signRequests,
+ registerRequests: registerRequests,
+ timeoutSeconds: timeoutSeconds,
+ requestId: reqId
+ };
+ }
+ // JS 1.1 API
+ return {
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+ appId: appId,
+ registerRequests: registerRequests,
+ registeredKeys: registeredKeys,
+ timeoutSeconds: timeoutSeconds,
+ requestId: reqId
+ };
+ };
+
+
+/**
+ * Posts a message on the underlying channel.
+ * @param {Object} message
+ */
+u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
+ this.port_.postMessage(message);
+};
+
+
+/**
+ * Emulates the HTML 5 addEventListener interface. Works only for the
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
+ function(eventName, handler) {
+ var name = eventName.toLowerCase();
+ if (name == 'message' || name == 'onmessage') {
+ this.port_.onMessage.addListener(function(message) {
+ // Emulate a minimal MessageEvent object
+ handler({'data': message});
+ });
+ } else {
+ console.error('WrappedChromeRuntimePort only supports onMessage');
+ }
+ };
+
+/**
+ * Wrap the Authenticator app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_ = function() {
+ this.requestId_ = -1;
+ this.requestObject_ = null;
+}
+
+/**
+ * Launch the Authenticator intent.
+ * @param {Object} message
+ */
+u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
+ var intentUrl =
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+ ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
+ ';end';
+ document.location = intentUrl;
+};
+
+/**
+ * Tells what type of port this is.
+ * @return {String} port type
+ */
+u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
+ return "WrappedAuthenticatorPort_";
+};
+
+
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
+ var name = eventName.toLowerCase();
+ if (name == 'message') {
+ var self = this;
+ /* Register a callback to that executes when
+ * chrome injects the response. */
+ window.addEventListener(
+ 'message', self.onRequestUpdate_.bind(self, handler), false);
+ } else {
+ console.error('WrappedAuthenticatorPort only supports message');
+ }
+};
+
+/**
+ * Callback invoked when a response is received from the Authenticator.
+ * @param function({data: Object}) callback
+ * @param {Object} message message Object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
+ function(callback, message) {
+ var messageObject = JSON.parse(message.data);
+ var intentUrl = messageObject['intentURL'];
+
+ var errorCode = messageObject['errorCode'];
+ var responseObject = null;
+ if (messageObject.hasOwnProperty('data')) {
+ responseObject = /** @type {Object} */ (
+ JSON.parse(messageObject['data']));
+ }
+
+ callback({'data': responseObject});
+ };
+
+/**
+ * Base URL for intents to Authenticator.
+ * @const
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
+ 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+
+/**
+ * Wrap the iOS client app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedIosPort_ = function() {};
+
+/**
+ * Launch the iOS client app request
+ * @param {Object} message
+ */
+u2f.WrappedIosPort_.prototype.postMessage = function(message) {
+ var str = JSON.stringify(message);
+ var url = "u2f://auth?" + encodeURI(str);
+ location.replace(url);
+};
+
+/**
+ * Tells what type of port this is.
+ * @return {String} port type
+ */
+u2f.WrappedIosPort_.prototype.getPortType = function() {
+ return "WrappedIosPort_";
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
+ var name = eventName.toLowerCase();
+ if (name !== 'message') {
+ console.error('WrappedIosPort only supports message');
+ }
+};
+
+/**
+ * Sets up an embedded trampoline iframe, sourced from the extension.
+ * @param {function(MessagePort)} callback
+ * @private
+ */
+u2f.getIframePort_ = function(callback) {
+ // Create the iframe
+ var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
+ var iframe = document.createElement('iframe');
+ iframe.src = iframeOrigin + '/u2f-comms.html';
+ iframe.setAttribute('style', 'display:none');
+ document.body.appendChild(iframe);
+
+ var channel = new MessageChannel();
+ var ready = function(message) {
+ if (message.data == 'ready') {
+ channel.port1.removeEventListener('message', ready);
+ callback(channel.port1);
+ } else {
+ console.error('First event on iframe port was not "ready"');
+ }
+ };
+ channel.port1.addEventListener('message', ready);
+ channel.port1.start();
+
+ iframe.addEventListener('load', function() {
+ // Deliver the port to the iframe and initialize
+ iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
+ });
+};
+
+
+//High-level JS API
+
+/**
+ * Default extension response timeout in seconds.
+ * @const
+ */
+u2f.EXTENSION_TIMEOUT_SEC = 30;
+
+/**
+ * A singleton instance for a MessagePort to the extension.
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
+ * @private
+ */
+u2f.port_ = null;
+
+/**
+ * Callbacks waiting for a port
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
+ * @private
+ */
+u2f.waitingForPort_ = [];
+
+/**
+ * A counter for requestIds.
+ * @type {number}
+ * @private
+ */
+u2f.reqCounter_ = 0;
+
+/**
+ * A map from requestIds to client callbacks
+ * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
+ * |function((u2f.Error|u2f.SignResponse)))>}
+ * @private
+ */
+u2f.callbackMap_ = {};
+
+/**
+ * Creates or retrieves the MessagePort singleton to use.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ * @private
+ */
+u2f.getPortSingleton_ = function(callback) {
+ if (u2f.port_) {
+ callback(u2f.port_);
+ } else {
+ if (u2f.waitingForPort_.length == 0) {
+ u2f.getMessagePort(function(port) {
+ u2f.port_ = port;
+ u2f.port_.addEventListener('message',
+ /** @type {function(Event)} */ (u2f.responseHandler_));
+
+ // Careful, here be async callbacks. Maybe.
+ while (u2f.waitingForPort_.length)
+ u2f.waitingForPort_.shift()(u2f.port_);
+ });
+ }
+ u2f.waitingForPort_.push(callback);
+ }
+};
+
+/**
+ * Handles response messages from the extension.
+ * @param {MessageEvent.<u2f.Response>} message
+ * @private
+ */
+u2f.responseHandler_ = function(message) {
+ var response = message.data;
+ var reqId = response['requestId'];
+ if (!reqId || !u2f.callbackMap_[reqId]) {
+ console.error('Unknown or missing requestId in response.');
+ return;
+ }
+ var cb = u2f.callbackMap_[reqId];
+ delete u2f.callbackMap_[reqId];
+ cb(response['responseData']);
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * If the JS API version supported by the extension is unknown, it first sends a
+ * message to the extension to find out the supported API version and then it sends
+ * the sign request.
+ * @param {string=} appId
+ * @param {string=} challenge
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
+ if (js_api_version === undefined) {
+ // Send a message to get the extension to JS API version, then send the actual sign request.
+ u2f.getApiVersion(
+ function (response) {
+ js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
+ console.log("Extension JS API Version: ", js_api_version);
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
+ });
+ } else {
+ // We know the JS API version. Send the actual sign request in the supported API version.
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
+ }
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * @param {string=} appId
+ * @param {string=} challenge
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
+ u2f.getPortSingleton_(function(port) {
+ var reqId = ++u2f.reqCounter_;
+ u2f.callbackMap_[reqId] = callback;
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+ var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
+ port.postMessage(req);
+ });
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * If the JS API version supported by the extension is unknown, it first sends a
+ * message to the extension to find out the supported API version and then it sends
+ * the register request.
+ * @param {string=} appId
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
+ if (js_api_version === undefined) {
+ // Send a message to get the extension to JS API version, then send the actual register request.
+ u2f.getApiVersion(
+ function (response) {
+ js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
+ console.log("Extension JS API Version: ", js_api_version);
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
+ callback, opt_timeoutSeconds);
+ });
+ } else {
+ // We know the JS API version. Send the actual register request in the supported API version.
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
+ callback, opt_timeoutSeconds);
+ }
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {string=} appId
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
+ u2f.getPortSingleton_(function(port) {
+ var reqId = ++u2f.reqCounter_;
+ u2f.callbackMap_[reqId] = callback;
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+ var req = u2f.formatRegisterRequest_(
+ appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
+ port.postMessage(req);
+ });
+};
+
+
+/**
+ * Dispatches a message to the extension to find out the supported
+ * JS API version.
+ * If the user is on a mobile phone and is thus using Google Authenticator instead
+ * of the Chrome extension, don't send the request and simply return 0.
+ * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
+ u2f.getPortSingleton_(function(port) {
+ // If we are using Android Google Authenticator or iOS client app,
+ // do not fire an intent to ask which JS API version to use.
+ if (port.getPortType) {
+ var apiVersion;
+ switch (port.getPortType()) {
+ case 'WrappedIosPort_':
+ case 'WrappedAuthenticatorPort_':
+ apiVersion = 1.1;
+ break;
+
+ default:
+ apiVersion = 0;
+ break;
+ }
+ callback({ 'js_api_version': apiVersion });
+ return;
+ }
+ var reqId = ++u2f.reqCounter_;
+ u2f.callbackMap_[reqId] = callback;
+ var req = {
+ type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
+ timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
+ requestId: reqId
+ };
+ port.postMessage(req);
+ });
+}; \ No newline at end of file
diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css
deleted file mode 100644
index b6f61295392..00000000000
--- a/vendor/assets/stylesheets/animate.css
+++ /dev/null
@@ -1,11 +0,0 @@
-@charset "UTF-8";
-
-/*!
- * animate.css -http://daneden.me/animate
- * Version - 3.5.1
- * Licensed under the MIT license - http://opensource.org/licenses/MIT
- *
- * Copyright (c) 2016 Daniel Eden
- */
-
-.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file
diff --git a/vendor/gitignore/Actionscript.gitignore b/vendor/gitignore/Actionscript.gitignore
new file mode 100644
index 00000000000..11e612e9853
--- /dev/null
+++ b/vendor/gitignore/Actionscript.gitignore
@@ -0,0 +1,19 @@
+# Build and Release Folders
+bin/
+bin-debug/
+bin-release/
+[Oo]bj/ # FlashDevelop obj
+[Bb]in/ # FlashDevelop bin
+
+# Other files and folders
+.settings/
+
+# Executables
+*.swf
+*.air
+*.ipa
+*.apk
+
+# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
+# should NOT be excluded as they contain compiler settings and other important
+# information for Eclipse / Flash Builder.
diff --git a/vendor/gitignore/Ada.gitignore b/vendor/gitignore/Ada.gitignore
new file mode 100644
index 00000000000..b4d703968a4
--- /dev/null
+++ b/vendor/gitignore/Ada.gitignore
@@ -0,0 +1,5 @@
+# Object file
+*.o
+
+# Ada Library Information
+*.ali
diff --git a/vendor/gitignore/Agda.gitignore b/vendor/gitignore/Agda.gitignore
new file mode 100644
index 00000000000..171a38976c1
--- /dev/null
+++ b/vendor/gitignore/Agda.gitignore
@@ -0,0 +1 @@
+*.agdai
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
new file mode 100644
index 00000000000..e5df7b9150e
--- /dev/null
+++ b/vendor/gitignore/Android.gitignore
@@ -0,0 +1,41 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Intellij
+*.iml
+.idea/workspace.xml
+.idea/libraries
+
+# Keystore files
+*.jks
diff --git a/vendor/gitignore/AppEngine.gitignore b/vendor/gitignore/AppEngine.gitignore
new file mode 100644
index 00000000000..62273454531
--- /dev/null
+++ b/vendor/gitignore/AppEngine.gitignore
@@ -0,0 +1,2 @@
+# Google App Engine generated folder
+appengine-generated/
diff --git a/vendor/gitignore/AppceleratorTitanium.gitignore b/vendor/gitignore/AppceleratorTitanium.gitignore
new file mode 100644
index 00000000000..3abea559761
--- /dev/null
+++ b/vendor/gitignore/AppceleratorTitanium.gitignore
@@ -0,0 +1,3 @@
+# Build folder and log file
+build/
+build.log
diff --git a/vendor/gitignore/ArchLinuxPackages.gitignore b/vendor/gitignore/ArchLinuxPackages.gitignore
new file mode 100644
index 00000000000..b73905529f2
--- /dev/null
+++ b/vendor/gitignore/ArchLinuxPackages.gitignore
@@ -0,0 +1,13 @@
+*.tar
+*.tar.*
+*.jar
+*.exe
+*.msi
+*.zip
+*.tgz
+*.log
+*.log.*
+*.sig
+
+pkg/
+src/
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
new file mode 100644
index 00000000000..1e9158e2a85
--- /dev/null
+++ b/vendor/gitignore/Autotools.gitignore
@@ -0,0 +1,18 @@
+# http://www.gnu.org/software/automake
+
+Makefile.in
+
+# http://www.gnu.org/software/autoconf
+
+/autom4te.cache
+/autoscan.log
+/autoscan-*.log
+/aclocal.m4
+/compile
+/config.h.in
+/configure
+/configure.scan
+/depcomp
+/install-sh
+/missing
+/stamp-h1
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
new file mode 100644
index 00000000000..259148fa18f
--- /dev/null
+++ b/vendor/gitignore/C++.gitignore
@@ -0,0 +1,32 @@
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore
new file mode 100644
index 00000000000..7a065c709c7
--- /dev/null
+++ b/vendor/gitignore/C.gitignore
@@ -0,0 +1,36 @@
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
diff --git a/vendor/gitignore/CFWheels.gitignore b/vendor/gitignore/CFWheels.gitignore
new file mode 100644
index 00000000000..f2fec34ff89
--- /dev/null
+++ b/vendor/gitignore/CFWheels.gitignore
@@ -0,0 +1,12 @@
+# unpacked plugin folders
+plugins/**/*
+
+# files directory where uploads go
+files
+
+# DBMigrate plugin: generated SQL
+db/sql
+
+# AssetBundler plugin: generated bundles
+javascripts/bundles
+stylesheets/bundles
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
new file mode 100644
index 00000000000..0cc7e4b5275
--- /dev/null
+++ b/vendor/gitignore/CMake.gitignore
@@ -0,0 +1,7 @@
+CMakeCache.txt
+CMakeFiles
+CMakeScripts
+Makefile
+cmake_install.cmake
+install_manifest.txt
+CTestTestfile.cmake
diff --git a/vendor/gitignore/CUDA.gitignore b/vendor/gitignore/CUDA.gitignore
new file mode 100644
index 00000000000..cb385db83fe
--- /dev/null
+++ b/vendor/gitignore/CUDA.gitignore
@@ -0,0 +1,6 @@
+*.i
+*.ii
+*.gpu
+*.ptx
+*.cubin
+*.fatbin
diff --git a/vendor/gitignore/CakePHP.gitignore b/vendor/gitignore/CakePHP.gitignore
new file mode 100644
index 00000000000..c6597e4eabf
--- /dev/null
+++ b/vendor/gitignore/CakePHP.gitignore
@@ -0,0 +1,25 @@
+# CakePHP 3
+
+/vendor/*
+/config/app.php
+
+/tmp/cache/models/*
+!/tmp/cache/models/empty
+/tmp/cache/persistent/*
+!/tmp/cache/persistent/empty
+/tmp/cache/views/*
+!/tmp/cache/views/empty
+/tmp/sessions/*
+!/tmp/sessions/empty
+/tmp/tests/*
+!/tmp/tests/empty
+
+/logs/*
+!/logs/empty
+
+# CakePHP 2
+
+/app/tmp/*
+/app/Config/core.php
+/app/Config/database.php
+/vendors/*
diff --git a/vendor/gitignore/ChefCookbook.gitignore b/vendor/gitignore/ChefCookbook.gitignore
new file mode 100644
index 00000000000..5ee7b7a9a18
--- /dev/null
+++ b/vendor/gitignore/ChefCookbook.gitignore
@@ -0,0 +1,9 @@
+.vagrant
+/cookbooks
+
+# Bundler
+bin/*
+.bundle/*
+
+.kitchen/
+.kitchen.local.yml
diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore
new file mode 120000
index 00000000000..7657a270c45
--- /dev/null
+++ b/vendor/gitignore/Clojure.gitignore
@@ -0,0 +1 @@
+Leiningen.gitignore \ No newline at end of file
diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore
new file mode 100644
index 00000000000..0f77d9e1d17
--- /dev/null
+++ b/vendor/gitignore/CodeIgniter.gitignore
@@ -0,0 +1,6 @@
+*/config/development
+*/logs/log-*.php
+!*/logs/index.html
+*/cache/*
+!*/cache/index.html
+!*/cache/.htaccess
diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore
new file mode 100644
index 00000000000..4806e580b60
--- /dev/null
+++ b/vendor/gitignore/CommonLisp.gitignore
@@ -0,0 +1,3 @@
+*.FASL
+*.fasl
+*.lisp-temp
diff --git a/vendor/gitignore/Composer.gitignore b/vendor/gitignore/Composer.gitignore
new file mode 100644
index 00000000000..c4222678424
--- /dev/null
+++ b/vendor/gitignore/Composer.gitignore
@@ -0,0 +1,6 @@
+composer.phar
+/vendor/
+
+# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# composer.lock
diff --git a/vendor/gitignore/Concrete5.gitignore b/vendor/gitignore/Concrete5.gitignore
new file mode 100644
index 00000000000..1fe53611e5d
--- /dev/null
+++ b/vendor/gitignore/Concrete5.gitignore
@@ -0,0 +1,4 @@
+config/site.php
+files/cache/*
+files/tmp/*
+.htaccess
diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore
new file mode 100644
index 00000000000..d3083b3a605
--- /dev/null
+++ b/vendor/gitignore/Coq.gitignore
@@ -0,0 +1,3 @@
+*.vo
+*.glob
+*.v.d
diff --git a/vendor/gitignore/CraftCMS.gitignore b/vendor/gitignore/CraftCMS.gitignore
new file mode 100644
index 00000000000..a70d4772c46
--- /dev/null
+++ b/vendor/gitignore/CraftCMS.gitignore
@@ -0,0 +1,3 @@
+# Craft Storage (cache) [http://buildwithcraft.com/help/craft-storage-gitignore]
+/craft/storage/*
+!/craft/storage/logo/* \ No newline at end of file
diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore
new file mode 100644
index 00000000000..74b926fc901
--- /dev/null
+++ b/vendor/gitignore/D.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files
+*.o
+*.obj
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Compiled Static libraries
+*.a
+*.lib
+
+# Executables
+*.exe
+
+# DUB
+.dub
+docs.json
+__dummy.html
+docs/
+
+# Code coverage
+*.lst
diff --git a/vendor/gitignore/DM.gitignore b/vendor/gitignore/DM.gitignore
new file mode 100644
index 00000000000..ba5abdab836
--- /dev/null
+++ b/vendor/gitignore/DM.gitignore
@@ -0,0 +1,5 @@
+*.dmb
+*.rsc
+*.int
+*.lk
+*.zip
diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore
new file mode 100644
index 00000000000..7c280441649
--- /dev/null
+++ b/vendor/gitignore/Dart.gitignore
@@ -0,0 +1,27 @@
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.buildlog
+.packages
+.project
+.pub/
+build/
+**/packages/
+
+# Files created by dart2js
+# (Most Dart developers will use pub build to compile Dart, use/modify these
+# rules if you intend to use dart2js directly
+# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
+# differentiate from explicit Javascript files)
+*.dart.js
+*.part.js
+*.js.deps
+*.js.map
+*.info.json
+
+# Directory created by dartdoc
+doc/api/
+
+# Don't commit pubspec lock file
+# (Library packages only! Remove pattern if developing an application package)
+pubspec.lock
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
new file mode 100644
index 00000000000..19864c6bbef
--- /dev/null
+++ b/vendor/gitignore/Delphi.gitignore
@@ -0,0 +1,66 @@
+# Uncomment these types if you want even more clean repository. But be careful.
+# It can make harm to an existing project source. Read explanations below.
+#
+# Resource files are binaries containing manifest, project icon and version info.
+# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files.
+#*.res
+#
+# Type library file (binary). In old Delphi versions it should be stored.
+# Since Delphi 2009 it is produced from .ridl file and can safely be ignored.
+#*.tlb
+#
+# Diagram Portfolio file. Used by the diagram editor up to Delphi 7.
+# Uncomment this if you are not using diagrams or use newer Delphi version.
+#*.ddp
+#
+# Visual LiveBindings file. Added in Delphi XE2.
+# Uncomment this if you are not using LiveBindings Designer.
+#*.vlb
+#
+# Deployment Manager configuration file for your project. Added in Delphi XE2.
+# Uncomment this if it is not mobile development and you do not use remote debug feature.
+#*.deployproj
+#
+# C++ object files produced when C/C++ Output file generation is configured.
+# Uncomment this if you are not using external objects (zlib library for example).
+#*.obj
+#
+
+# Delphi compiler-generated binaries (safe to delete)
+*.exe
+*.dll
+*.bpl
+*.bpi
+*.dcp
+*.so
+*.apk
+*.drc
+*.map
+*.dres
+*.rsm
+*.tds
+*.dcu
+*.lib
+*.a
+*.o
+*.ocx
+
+# Delphi autogenerated files (duplicated info)
+*.cfg
+*.hpp
+*Resource.rc
+
+# Delphi local files (user-specific info)
+*.local
+*.identcache
+*.projdata
+*.tvsconfig
+*.dsk
+
+# Delphi history and backups
+__history/
+__recovery/
+*.~*
+
+# Castalia statistics file (since XE7 Castalia is distributed with Delphi)
+*.stat
diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore
new file mode 100644
index 00000000000..0d2fe537f46
--- /dev/null
+++ b/vendor/gitignore/Drupal.gitignore
@@ -0,0 +1,36 @@
+# Ignore configuration files that may contain sensitive information.
+sites/*/*settings*.php
+
+# Ignore paths that contain generated content.
+files/
+sites/*/files
+sites/*/private
+
+# Ignore default text files
+robots.txt
+/CHANGELOG.txt
+/COPYRIGHT.txt
+/INSTALL*.txt
+/LICENSE.txt
+/MAINTAINERS.txt
+/UPGRADE.txt
+/README.txt
+sites/README.txt
+sites/all/modules/README.txt
+sites/all/themes/README.txt
+
+# Ignore everything but the "sites" folder ( for non core developer )
+.htaccess
+web.config
+authorize.php
+cron.php
+index.php
+install.php
+update.php
+xmlrpc.php
+/includes
+/misc
+/modules
+/profiles
+/scripts
+/themes
diff --git a/vendor/gitignore/EPiServer.gitignore b/vendor/gitignore/EPiServer.gitignore
new file mode 100644
index 00000000000..97037de743e
--- /dev/null
+++ b/vendor/gitignore/EPiServer.gitignore
@@ -0,0 +1,4 @@
+######################
+## EPiServer Files
+######################
+*License.config
diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore
new file mode 100644
index 00000000000..9ced1260266
--- /dev/null
+++ b/vendor/gitignore/Eagle.gitignore
@@ -0,0 +1,44 @@
+# Ignore list for Eagle, a PCB layout tool
+
+# Backup files
+*.s#?
+*.b#?
+*.l#?
+
+# Eagle project file
+# It contains a serial number and references to the file structure
+# on your computer.
+# comment the following line if you want to have your project file included.
+eagle.epf
+
+# Autorouter files
+*.pro
+*.job
+
+# CAM files
+*.$$$
+*.cmp
+*.ly2
+*.l15
+*.sol
+*.plc
+*.stc
+*.sts
+*.crc
+*.crs
+
+*.dri
+*.drl
+*.gpi
+*.pls
+
+*.drd
+*.drd.*
+
+*.info
+
+*.eps
+
+# file locks introduced since 7.x
+*.lck
+
diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore
new file mode 100644
index 00000000000..9b4291b7fe8
--- /dev/null
+++ b/vendor/gitignore/Elisp.gitignore
@@ -0,0 +1,5 @@
+# Compiled
+*.elc
+
+# Packaging
+.cask
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
new file mode 100644
index 00000000000..755b605549d
--- /dev/null
+++ b/vendor/gitignore/Elixir.gitignore
@@ -0,0 +1,5 @@
+/_build
+/cover
+/deps
+erl_crash.dump
+*.ez
diff --git a/vendor/gitignore/Elm.gitignore b/vendor/gitignore/Elm.gitignore
new file mode 100644
index 00000000000..a594364e2c0
--- /dev/null
+++ b/vendor/gitignore/Elm.gitignore
@@ -0,0 +1,4 @@
+# elm-package generated files
+elm-stuff/
+# elm-repl generated files
+repl-temp-*
diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore
new file mode 100644
index 00000000000..8e46d5a07f8
--- /dev/null
+++ b/vendor/gitignore/Erlang.gitignore
@@ -0,0 +1,10 @@
+.eunit
+deps
+*.o
+*.beam
+*.plt
+erl_crash.dump
+ebin
+rel/example_project
+.concrete/DEV_MODE
+.rebar
diff --git a/vendor/gitignore/ExpressionEngine.gitignore b/vendor/gitignore/ExpressionEngine.gitignore
new file mode 100644
index 00000000000..314e4df123a
--- /dev/null
+++ b/vendor/gitignore/ExpressionEngine.gitignore
@@ -0,0 +1,19 @@
+.DS_Store
+
+# Images
+images/avatars/
+images/captchas/
+images/smileys/
+images/member_photos/
+images/signature_attachments/
+images/pm_attachments/
+
+# For security do not publish the following files
+system/expressionengine/config/database.php
+system/expressionengine/config/config.php
+
+# Caches
+sized/
+thumbs/
+_thumbs/
+*/expressionengine/cache/*
diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore
new file mode 100644
index 00000000000..5ffc21546ec
--- /dev/null
+++ b/vendor/gitignore/ExtJs.gitignore
@@ -0,0 +1,4 @@
+.architect
+bootstrap.json
+build/
+ext/
diff --git a/vendor/gitignore/Fancy.gitignore b/vendor/gitignore/Fancy.gitignore
new file mode 100644
index 00000000000..70d6e631e55
--- /dev/null
+++ b/vendor/gitignore/Fancy.gitignore
@@ -0,0 +1,2 @@
+*.rbc
+*.fyc
diff --git a/vendor/gitignore/Finale.gitignore b/vendor/gitignore/Finale.gitignore
new file mode 100644
index 00000000000..7ef08e0c343
--- /dev/null
+++ b/vendor/gitignore/Finale.gitignore
@@ -0,0 +1,13 @@
+*.bak
+*.db
+*.avi
+*.pdf
+*.ps
+*.mid
+*.midi
+*.mp3
+*.aif
+*.wav
+# Some versions of Finale have a bug and randomly save extra copies of
+# the music source as "<Filename> copy.mus"
+*copy.mus
diff --git a/vendor/gitignore/ForceDotCom.gitignore b/vendor/gitignore/ForceDotCom.gitignore
new file mode 100644
index 00000000000..3933cd4dd50
--- /dev/null
+++ b/vendor/gitignore/ForceDotCom.gitignore
@@ -0,0 +1,4 @@
+.project
+.settings
+salesforce.schema
+Referenced Packages
diff --git a/vendor/gitignore/Fortran.gitignore b/vendor/gitignore/Fortran.gitignore
new file mode 120000
index 00000000000..5daba98a3e6
--- /dev/null
+++ b/vendor/gitignore/Fortran.gitignore
@@ -0,0 +1 @@
+C++.gitignore \ No newline at end of file
diff --git a/vendor/gitignore/FuelPHP.gitignore b/vendor/gitignore/FuelPHP.gitignore
new file mode 100644
index 00000000000..d69f71f4338
--- /dev/null
+++ b/vendor/gitignore/FuelPHP.gitignore
@@ -0,0 +1,21 @@
+# the composer package lock file and install directory
+# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# /composer.lock
+/fuel/vendor
+
+# the fuelphp document
+/docs/
+
+# you may install these packages with `oil package`.
+# http://fuelphp.com/docs/packages/oil/package.html
+# /fuel/packages/auth/
+# /fuel/packages/email/
+# /fuel/packages/oil/
+# /fuel/packages/orm/
+# /fuel/packages/parser/
+
+# dynamically generated files
+/fuel/app/logs/*/*/*
+/fuel/app/cache/*/*
+/fuel/app/config/crypt.php
diff --git a/vendor/gitignore/GWT.gitignore b/vendor/gitignore/GWT.gitignore
new file mode 100644
index 00000000000..07704e54bbc
--- /dev/null
+++ b/vendor/gitignore/GWT.gitignore
@@ -0,0 +1,28 @@
+*.class
+
+# Package Files #
+*.jar
+*.war
+
+# gwt caches and compiled units #
+war/gwt_bree/
+gwt-unitCache/
+
+# boilerplate generated classes #
+.apt_generated/
+
+# more caches and things from deploy #
+war/WEB-INF/deploy/
+war/WEB-INF/classes/
+
+#compilation logs
+.gwt/
+
+#caching for already compiled files
+gwt-unitCache/
+
+#gwt junit compilation files
+www-test/
+
+#old GWT (1.5) created this dir
+.gwt-tmp/
diff --git a/vendor/gitignore/Gcov.gitignore b/vendor/gitignore/Gcov.gitignore
new file mode 100644
index 00000000000..a6451430e17
--- /dev/null
+++ b/vendor/gitignore/Gcov.gitignore
@@ -0,0 +1,5 @@
+# gcc coverage testing tool files
+
+*.gcno
+*.gcda
+*.gcov
diff --git a/vendor/gitignore/GitBook.gitignore b/vendor/gitignore/GitBook.gitignore
new file mode 100644
index 00000000000..4cb12d8db77
--- /dev/null
+++ b/vendor/gitignore/GitBook.gitignore
@@ -0,0 +1,16 @@
+# Node rules:
+## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+## Dependency directory
+## Commenting this out is preferred by some people, see
+## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
+node_modules
+
+# Book build output
+_book
+
+# eBook build output
+*.epub
+*.mobi
+*.pdf
diff --git a/vendor/gitignore/Global/Anjuta.gitignore b/vendor/gitignore/Global/Anjuta.gitignore
new file mode 100644
index 00000000000..20dd42c53e6
--- /dev/null
+++ b/vendor/gitignore/Global/Anjuta.gitignore
@@ -0,0 +1,3 @@
+# Local configuration folder and symbol database
+/.anjuta/
+/.anjuta_sym_db.db
diff --git a/vendor/gitignore/Global/Archives.gitignore b/vendor/gitignore/Global/Archives.gitignore
new file mode 100644
index 00000000000..e9eda68baf2
--- /dev/null
+++ b/vendor/gitignore/Global/Archives.gitignore
@@ -0,0 +1,27 @@
+# It's better to unpack these files and commit the raw source because
+# git has its own built in compression methods.
+*.7z
+*.jar
+*.rar
+*.zip
+*.gz
+*.bzip
+*.bz2
+*.xz
+*.lzma
+*.cab
+
+#packing-only formats
+*.iso
+*.tar
+
+#package management formats
+*.dmg
+*.xpi
+*.gem
+*.egg
+*.deb
+*.rpm
+*.msi
+*.msm
+*.msp
diff --git a/vendor/gitignore/Global/Bazaar.gitignore b/vendor/gitignore/Global/Bazaar.gitignore
new file mode 100644
index 00000000000..3cbbcbd11ec
--- /dev/null
+++ b/vendor/gitignore/Global/Bazaar.gitignore
@@ -0,0 +1,2 @@
+.bzr/
+.bzrignore
diff --git a/vendor/gitignore/Global/BricxCC.gitignore b/vendor/gitignore/Global/BricxCC.gitignore
new file mode 100644
index 00000000000..c1d16a46c98
--- /dev/null
+++ b/vendor/gitignore/Global/BricxCC.gitignore
@@ -0,0 +1,4 @@
+# Bricx Command Center IDE
+# http://bricxcc.sourceforge.net
+*.bak
+*.sym
diff --git a/vendor/gitignore/Global/CVS.gitignore b/vendor/gitignore/Global/CVS.gitignore
new file mode 100644
index 00000000000..1695352e146
--- /dev/null
+++ b/vendor/gitignore/Global/CVS.gitignore
@@ -0,0 +1,4 @@
+/CVS/*
+**/CVS/*
+.cvsignore
+*/.cvsignore
diff --git a/vendor/gitignore/Global/Calabash.gitignore b/vendor/gitignore/Global/Calabash.gitignore
new file mode 100644
index 00000000000..8a75b329dcd
--- /dev/null
+++ b/vendor/gitignore/Global/Calabash.gitignore
@@ -0,0 +1,10 @@
+# Calabash / Cucumber
+rerun/
+reports/
+screenshots/
+screenshot*.png
+test-servers/
+
+# bundler
+.bundle
+vendor
diff --git a/vendor/gitignore/Global/Cloud9.gitignore b/vendor/gitignore/Global/Cloud9.gitignore
new file mode 100644
index 00000000000..3f4384df508
--- /dev/null
+++ b/vendor/gitignore/Global/Cloud9.gitignore
@@ -0,0 +1,3 @@
+# Cloud9 IDE - http://c9.io
+.c9revisions
+.c9
diff --git a/vendor/gitignore/Global/CodeKit.gitignore b/vendor/gitignore/Global/CodeKit.gitignore
new file mode 100644
index 00000000000..bd9e67fcca2
--- /dev/null
+++ b/vendor/gitignore/Global/CodeKit.gitignore
@@ -0,0 +1,3 @@
+# General CodeKit files to ignore
+config.codekit
+/min
diff --git a/vendor/gitignore/Global/DartEditor.gitignore b/vendor/gitignore/Global/DartEditor.gitignore
new file mode 100644
index 00000000000..948920b420e
--- /dev/null
+++ b/vendor/gitignore/Global/DartEditor.gitignore
@@ -0,0 +1,2 @@
+.project
+.buildlog
diff --git a/vendor/gitignore/Global/Dreamweaver.gitignore b/vendor/gitignore/Global/Dreamweaver.gitignore
new file mode 100644
index 00000000000..0621a3d53b5
--- /dev/null
+++ b/vendor/gitignore/Global/Dreamweaver.gitignore
@@ -0,0 +1,7 @@
+# DW Dreamweaver added files
+_notes
+_compareTemp
+configs/
+dwsync.xml
+dw_php_codehinting.config
+*.mno
diff --git a/vendor/gitignore/Global/Dropbox.gitignore b/vendor/gitignore/Global/Dropbox.gitignore
new file mode 100644
index 00000000000..40f4a469d25
--- /dev/null
+++ b/vendor/gitignore/Global/Dropbox.gitignore
@@ -0,0 +1,4 @@
+# Dropbox settings and caches
+.dropbox
+.dropbox.attr
+.dropbox.cache
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
new file mode 100644
index 00000000000..31c9fb31167
--- /dev/null
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -0,0 +1,51 @@
+
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# Eclipse Core
+.project
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
diff --git a/vendor/gitignore/Global/EiffelStudio.gitignore b/vendor/gitignore/Global/EiffelStudio.gitignore
new file mode 100644
index 00000000000..f41b4f70216
--- /dev/null
+++ b/vendor/gitignore/Global/EiffelStudio.gitignore
@@ -0,0 +1,2 @@
+# The compilation directory
+EIFGENs
diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore
new file mode 100644
index 00000000000..0c96c9ad060
--- /dev/null
+++ b/vendor/gitignore/Global/Emacs.gitignore
@@ -0,0 +1,42 @@
+# -*- mode: gitignore; -*-
+*~
+\#*\#
+/.emacs.desktop
+/.emacs.desktop.lock
+*.elc
+auto-save-list
+tramp
+.\#*
+
+# Org-mode
+.org-id-locations
+*_archive
+
+# flymake-mode
+*_flymake.*
+
+# eshell files
+/eshell/history
+/eshell/lastdir
+
+# elpa packages
+/elpa/
+
+# reftex files
+*.rel
+
+# AUCTeX auto folder
+/auto/
+
+# cask packages
+.cask/
+dist/
+
+# Flycheck
+flycheck_*.el
+
+# server auth directory
+/server/
+
+# projectiles files
+.projectile \ No newline at end of file
diff --git a/vendor/gitignore/Global/Ensime.gitignore b/vendor/gitignore/Global/Ensime.gitignore
new file mode 100644
index 00000000000..f2daebb9f4b
--- /dev/null
+++ b/vendor/gitignore/Global/Ensime.gitignore
@@ -0,0 +1,4 @@
+# Ensime specific
+.ensime
+.ensime_cache/
+.ensime_lucene/
diff --git a/vendor/gitignore/Global/Espresso.gitignore b/vendor/gitignore/Global/Espresso.gitignore
new file mode 100644
index 00000000000..1234530b5b3
--- /dev/null
+++ b/vendor/gitignore/Global/Espresso.gitignore
@@ -0,0 +1 @@
+*.esproj
diff --git a/vendor/gitignore/Global/FlexBuilder.gitignore b/vendor/gitignore/Global/FlexBuilder.gitignore
new file mode 100644
index 00000000000..bbbfb91d9eb
--- /dev/null
+++ b/vendor/gitignore/Global/FlexBuilder.gitignore
@@ -0,0 +1,3 @@
+bin/
+bin-debug/
+bin-release/
diff --git a/vendor/gitignore/Global/GPG.gitignore b/vendor/gitignore/Global/GPG.gitignore
new file mode 100644
index 00000000000..7740a01538c
--- /dev/null
+++ b/vendor/gitignore/Global/GPG.gitignore
@@ -0,0 +1,2 @@
+secring.*
+
diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore
new file mode 100644
index 00000000000..27c13510bf5
--- /dev/null
+++ b/vendor/gitignore/Global/IPythonNotebook.gitignore
@@ -0,0 +1,2 @@
+# Temporary data
+.ipynb_checkpoints/
diff --git a/vendor/gitignore/Global/JDeveloper.gitignore b/vendor/gitignore/Global/JDeveloper.gitignore
new file mode 100644
index 00000000000..5bba6f37733
--- /dev/null
+++ b/vendor/gitignore/Global/JDeveloper.gitignore
@@ -0,0 +1,13 @@
+# default application storage directory used by the IDE Performance Cache feature
+.data/
+
+# used for ADF styles caching
+temp/
+
+# default output directories
+classes/
+deploy/
+javadoc/
+
+# lock file, a part of Oracle Credential Store Framework
+cwallet.sso.lck \ No newline at end of file
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
new file mode 100644
index 00000000000..ea83a5eb620
--- /dev/null
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -0,0 +1,44 @@
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/vendor/gitignore/Global/KDevelop4.gitignore b/vendor/gitignore/Global/KDevelop4.gitignore
new file mode 100644
index 00000000000..7ac57b1add4
--- /dev/null
+++ b/vendor/gitignore/Global/KDevelop4.gitignore
@@ -0,0 +1,2 @@
+*.kdev4
+.kdev4/
diff --git a/vendor/gitignore/Global/Kate.gitignore b/vendor/gitignore/Global/Kate.gitignore
new file mode 100644
index 00000000000..7ff06ce5390
--- /dev/null
+++ b/vendor/gitignore/Global/Kate.gitignore
@@ -0,0 +1,3 @@
+# Swap Files #
+.*.kate-swp
+.swp.*
diff --git a/vendor/gitignore/Global/Lazarus.gitignore b/vendor/gitignore/Global/Lazarus.gitignore
new file mode 100644
index 00000000000..b32943f1c6e
--- /dev/null
+++ b/vendor/gitignore/Global/Lazarus.gitignore
@@ -0,0 +1,30 @@
+# Lazarus compiler-generated binaries (safe to delete)
+*.exe
+*.dll
+*.so
+*.dylib
+*.lrs
+*.res
+*.compiled
+*.dbg
+*.ppu
+*.o
+*.or
+*.a
+
+# Lazarus autogenerated files (duplicated info)
+*.rst
+*.rsj
+*.lrt
+
+# Lazarus local files (user-specific info)
+*.lps
+
+# Lazarus backups and unit output folders.
+# These can be changed by user in Lazarus/project options.
+backup/
+*.bak
+lib/
+
+# Application bundle for Mac OS
+*.app/
diff --git a/vendor/gitignore/Global/LibreOffice.gitignore b/vendor/gitignore/Global/LibreOffice.gitignore
new file mode 100644
index 00000000000..586beac91d3
--- /dev/null
+++ b/vendor/gitignore/Global/LibreOffice.gitignore
@@ -0,0 +1,2 @@
+# LibreOffice locks
+.~lock.*#
diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore
new file mode 100644
index 00000000000..cc9586893b6
--- /dev/null
+++ b/vendor/gitignore/Global/Linux.gitignore
@@ -0,0 +1,10 @@
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
diff --git a/vendor/gitignore/Global/LyX.gitignore b/vendor/gitignore/Global/LyX.gitignore
new file mode 100644
index 00000000000..8efe0195cf3
--- /dev/null
+++ b/vendor/gitignore/Global/LyX.gitignore
@@ -0,0 +1,4 @@
+# Ignore LyX backup and autosave files
+# http://www.lyx.org/
+*.lyx~
+*.lyx#
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
new file mode 100644
index 00000000000..32a5ad4c777
--- /dev/null
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -0,0 +1,19 @@
+##---------------------------------------------------
+## Remove autosaves generated by the Matlab editor
+## We have git for backups!
+##---------------------------------------------------
+
+# Windows default autosave extension
+*.asv
+
+# OSX / *nix default autosave extension
+*.m~
+
+# Compiled MEX binaries (all platforms)
+*.mex*
+
+# Simulink Code Generation
+slprj/
+
+# Session info
+octave-workspace
diff --git a/vendor/gitignore/Global/Mercurial.gitignore b/vendor/gitignore/Global/Mercurial.gitignore
new file mode 100644
index 00000000000..e65d1137988
--- /dev/null
+++ b/vendor/gitignore/Global/Mercurial.gitignore
@@ -0,0 +1,6 @@
+.hg/
+.hgignore
+.hgsigs
+.hgsub
+.hgsubstate
+.hgtags
diff --git a/vendor/gitignore/Global/MicrosoftOffice.gitignore b/vendor/gitignore/Global/MicrosoftOffice.gitignore
new file mode 100644
index 00000000000..cb891745660
--- /dev/null
+++ b/vendor/gitignore/Global/MicrosoftOffice.gitignore
@@ -0,0 +1,16 @@
+*.tmp
+
+# Word temporary
+~$*.doc*
+
+# Excel temporary
+~$*.xls*
+
+# Excel Backup File
+*.xlk
+
+# PowerPoint temporary
+~$*.ppt*
+
+# Visio autosave temporary files
+*.~vsdx
diff --git a/vendor/gitignore/Global/ModelSim.gitignore b/vendor/gitignore/Global/ModelSim.gitignore
new file mode 100644
index 00000000000..46592b86430
--- /dev/null
+++ b/vendor/gitignore/Global/ModelSim.gitignore
@@ -0,0 +1,23 @@
+# ignore ModelSim generated files and directories (temp files and so on)
+[_@]*
+
+# ignore compilation output of ModelSim
+*.mti
+*.dat
+*.dbs
+*.psm
+*.bak
+*.cmp
+*.jpg
+*.html
+*.bsf
+
+# ignore simulation output of ModelSim
+wlf*
+*.wlf
+*.vstf
+*.ucdb
+cov*/
+transcript*
+sc_dpiheader.h
+vsim.dbg
diff --git a/vendor/gitignore/Global/Momentics.gitignore b/vendor/gitignore/Global/Momentics.gitignore
new file mode 100644
index 00000000000..b14db2d8645
--- /dev/null
+++ b/vendor/gitignore/Global/Momentics.gitignore
@@ -0,0 +1,8 @@
+# Built files
+x86/
+arm/
+arm-p/
+translations/*.qm
+
+# IDE settings
+.settings/
diff --git a/vendor/gitignore/Global/MonoDevelop.gitignore b/vendor/gitignore/Global/MonoDevelop.gitignore
new file mode 100644
index 00000000000..ef38d06b08f
--- /dev/null
+++ b/vendor/gitignore/Global/MonoDevelop.gitignore
@@ -0,0 +1,8 @@
+#User Specific
+*.userprefs
+*.usertasks
+
+#Mono Project Files
+*.pidb
+*.resources
+test-results/
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
new file mode 100644
index 00000000000..520d91ff584
--- /dev/null
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -0,0 +1,7 @@
+nbproject/private/
+build/
+nbbuild/
+dist/
+nbdist/
+nbactions.xml
+.nb-gradle/
diff --git a/vendor/gitignore/Global/Ninja.gitignore b/vendor/gitignore/Global/Ninja.gitignore
new file mode 100644
index 00000000000..50e58f24cc9
--- /dev/null
+++ b/vendor/gitignore/Global/Ninja.gitignore
@@ -0,0 +1,2 @@
+.ninja_deps
+.ninja_log
diff --git a/vendor/gitignore/Global/NotepadPP.gitignore b/vendor/gitignore/Global/NotepadPP.gitignore
new file mode 100644
index 00000000000..8fbda83a2c9
--- /dev/null
+++ b/vendor/gitignore/Global/NotepadPP.gitignore
@@ -0,0 +1,2 @@
+# Notepad++ backups #
+*.bak
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore
new file mode 100644
index 00000000000..5972fe50f66
--- /dev/null
+++ b/vendor/gitignore/Global/OSX.gitignore
@@ -0,0 +1,25 @@
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/vendor/gitignore/Global/Otto.gitignore b/vendor/gitignore/Global/Otto.gitignore
new file mode 100644
index 00000000000..5aa263f9db0
--- /dev/null
+++ b/vendor/gitignore/Global/Otto.gitignore
@@ -0,0 +1 @@
+.otto/
diff --git a/vendor/gitignore/Global/README.md b/vendor/gitignore/Global/README.md
new file mode 100644
index 00000000000..06b6649bd9a
--- /dev/null
+++ b/vendor/gitignore/Global/README.md
@@ -0,0 +1,10 @@
+## Globally Useful gitignores
+
+This directory contains globally useful gitignores,
+e.g. OS-specific and editor specific.
+
+For more on global gitignores:
+<https://help.github.com/articles/ignoring-files/#create-a-global-gitignore>
+
+And a good blog post about 'em:
+<http://augustl.com/blog/2009/global_gitignores>
diff --git a/vendor/gitignore/Global/Redcar.gitignore b/vendor/gitignore/Global/Redcar.gitignore
new file mode 100644
index 00000000000..b4a9d1d68e3
--- /dev/null
+++ b/vendor/gitignore/Global/Redcar.gitignore
@@ -0,0 +1 @@
+.redcar
diff --git a/vendor/gitignore/Global/Redis.gitignore b/vendor/gitignore/Global/Redis.gitignore
new file mode 100644
index 00000000000..57c1c230f92
--- /dev/null
+++ b/vendor/gitignore/Global/Redis.gitignore
@@ -0,0 +1,3 @@
+# Ignore redis binary dump (dump.rdb) files
+
+*.rdb
diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore
new file mode 100644
index 00000000000..970d897c75c
--- /dev/null
+++ b/vendor/gitignore/Global/SBT.gitignore
@@ -0,0 +1,9 @@
+# Simple Build Tool
+# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
+
+target/
+lib_managed/
+src_managed/
+project/boot/
+.history
+.cache
diff --git a/vendor/gitignore/Global/SVN.gitignore b/vendor/gitignore/Global/SVN.gitignore
new file mode 100644
index 00000000000..1b53ace613f
--- /dev/null
+++ b/vendor/gitignore/Global/SVN.gitignore
@@ -0,0 +1 @@
+.svn/
diff --git a/vendor/gitignore/Global/SlickEdit.gitignore b/vendor/gitignore/Global/SlickEdit.gitignore
new file mode 100644
index 00000000000..f30b8da457c
--- /dev/null
+++ b/vendor/gitignore/Global/SlickEdit.gitignore
@@ -0,0 +1,11 @@
+# SlickEdit workspace and project files are ignored by default because
+# typically they are considered to be developer-specific and not part of a
+# project.
+*.vpw
+*.vpj
+
+# SlickEdit workspace history and tag files always contain user-specific
+# data so they should not be stored in a repository.
+*.vpwhistu
+*.vpwhist
+*.vtg
diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore
new file mode 100644
index 00000000000..69c8c2b29ce
--- /dev/null
+++ b/vendor/gitignore/Global/SublimeText.gitignore
@@ -0,0 +1,27 @@
+# cache files for sublime text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+# workspace files are user-specific
+*.sublime-workspace
+
+# project files should be checked into the repository, unless a significant
+# proportion of contributors will probably not be using SublimeText
+# *.sublime-project
+
+# sftp configuration file
+sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings
diff --git a/vendor/gitignore/Global/SynopsysVCS.gitignore b/vendor/gitignore/Global/SynopsysVCS.gitignore
new file mode 100644
index 00000000000..eed2432fb78
--- /dev/null
+++ b/vendor/gitignore/Global/SynopsysVCS.gitignore
@@ -0,0 +1,36 @@
+# Waveform formats
+*.vcd
+*.vpd
+*.evcd
+*.fsdb
+
+# Default name of the simulation executable. A different name can be
+# specified with this switch (the associated daidir database name is
+# also taken from here): -o <path>/<filename>
+simv
+
+# Generated for Verilog and VHDL top configs
+simv.daidir/
+simv.db.dir/
+
+# Infrastructure necessary to co-simulate SystemC models with
+# Verilog/VHDL models. An alternate directory may be specified with this
+# switch: -Mdir=<directory_path>
+csrc/
+
+# Log file - the following switch allows to specify the file that will be
+# used to write all messages from simulation: -l <filename>
+*.log
+
+# Coverage results (generated with urg) and database location. The
+# following switch can also be used: urg -dir <coverage_directory>.vdb
+simv.vdb/
+urgReport/
+
+# DVE and UCLI related files.
+DVEfiles/
+ucli.key
+
+# When the design is elaborated for DirectC, the following file is created
+# with declarations for C/C++ functions.
+vc_hdrs.h
diff --git a/vendor/gitignore/Global/Tags.gitignore b/vendor/gitignore/Global/Tags.gitignore
new file mode 100644
index 00000000000..c0318165a27
--- /dev/null
+++ b/vendor/gitignore/Global/Tags.gitignore
@@ -0,0 +1,16 @@
+# Ignore tags created by etags, ctags, gtags (GNU global) and cscope
+TAGS
+.TAGS
+!TAGS/
+tags
+.tags
+!tags/
+gtags.files
+GTAGS
+GRTAGS
+GPATH
+cscope.files
+cscope.out
+cscope.in.out
+cscope.po.out
+
diff --git a/vendor/gitignore/Global/TextMate.gitignore b/vendor/gitignore/Global/TextMate.gitignore
new file mode 100644
index 00000000000..41e8d07a940
--- /dev/null
+++ b/vendor/gitignore/Global/TextMate.gitignore
@@ -0,0 +1,3 @@
+*.tmproj
+*.tmproject
+tmtags
diff --git a/vendor/gitignore/Global/TortoiseGit.gitignore b/vendor/gitignore/Global/TortoiseGit.gitignore
new file mode 100644
index 00000000000..db89590a629
--- /dev/null
+++ b/vendor/gitignore/Global/TortoiseGit.gitignore
@@ -0,0 +1,2 @@
+# Project-level settings
+/.tgitconfig
diff --git a/vendor/gitignore/Global/Vagrant.gitignore b/vendor/gitignore/Global/Vagrant.gitignore
new file mode 100644
index 00000000000..a977916f658
--- /dev/null
+++ b/vendor/gitignore/Global/Vagrant.gitignore
@@ -0,0 +1 @@
+.vagrant/
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
new file mode 100644
index 00000000000..bdc04a0b529
--- /dev/null
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -0,0 +1,10 @@
+# swap
+[._]*.s[a-w][a-z]
+[._]s[a-w][a-z]
+# session
+Session.vim
+# temporary
+.netrwhist
+*~
+# auto-generated tag files
+tags
diff --git a/vendor/gitignore/Global/VirtualEnv.gitignore b/vendor/gitignore/Global/VirtualEnv.gitignore
new file mode 100644
index 00000000000..b2c22f2af7f
--- /dev/null
+++ b/vendor/gitignore/Global/VirtualEnv.gitignore
@@ -0,0 +1,12 @@
+# Virtualenv
+# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
+.Python
+[Bb]in
+[Ii]nclude
+[Ll]ib
+[Ll]ib64
+[Ll]ocal
+[Ss]cripts
+pyvenv.cfg
+.venv
+pip-selfcheck.json
diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore
new file mode 100644
index 00000000000..faa18382a3c
--- /dev/null
+++ b/vendor/gitignore/Global/VisualStudioCode.gitignore
@@ -0,0 +1,2 @@
+.vscode
+
diff --git a/vendor/gitignore/Global/WebMethods.gitignore b/vendor/gitignore/Global/WebMethods.gitignore
new file mode 100644
index 00000000000..b383c25ca3c
--- /dev/null
+++ b/vendor/gitignore/Global/WebMethods.gitignore
@@ -0,0 +1,14 @@
+**/IntegrationServer/datastore/
+**/IntegrationServer/db/
+**/IntegrationServer/DocumentStore/
+**/IntegrationServer/lib/
+**/IntegrationServer/logs/
+**/IntegrationServer/replicate/
+**/IntegrationServer/sdk/
+**/IntegrationServer/support/
+**/IntegrationServer/update/
+**/IntegrationServer/userFtpRoot/
+**/IntegrationServer/web/
+**/IntegrationServer/WmRepository4/
+**/IntegrationServer/XAStore/
+**/IntegrationServer/packages/Wm*/
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
new file mode 100644
index 00000000000..a0d31452b0e
--- /dev/null
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -0,0 +1,18 @@
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
new file mode 100644
index 00000000000..37de8bb4793
--- /dev/null
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -0,0 +1,23 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
diff --git a/vendor/gitignore/Global/XilinxISE.gitignore b/vendor/gitignore/Global/XilinxISE.gitignore
new file mode 100644
index 00000000000..4475f843da9
--- /dev/null
+++ b/vendor/gitignore/Global/XilinxISE.gitignore
@@ -0,0 +1,67 @@
+# intermediate build files
+*.bgn
+*.bit
+*.bld
+*.cmd_log
+*.drc
+*.ll
+*.lso
+*.msd
+*.msk
+*.ncd
+*.ngc
+*.ngd
+*.ngr
+*.pad
+*.par
+*.pcf
+*.prj
+*.ptwx
+*.rbb
+*.rbd
+*.stx
+*.syr
+*.twr
+*.twx
+*.unroutes
+*.ut
+*.xpi
+*.xst
+*_bitgen.xwbt
+*_envsettings.html
+*_map.map
+*_map.mrp
+*_map.ngm
+*_map.xrpt
+*_ngdbuild.xrpt
+*_pad.csv
+*_pad.txt
+*_par.xrpt
+*_summary.html
+*_summary.xml
+*_usage.xml
+*_xst.xrpt
+
+# iMPACT generated files
+_impactbatch.log
+impact.xsl
+impact_impact.xwbt
+ise_impact.cmd
+webtalk_impact.xml
+
+# Core Generator generated files
+xaw2verilog.log
+
+# project-wide generated files
+*.gise
+par_usage_statistics.html
+usage_statistics_webtalk.html
+webtalk.log
+webtalk_pn.xml
+
+# generated folders
+iseconfig/
+xlnx_auto_0_xdb/
+xst/
+_ngo/
+_xmsgs/
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
new file mode 100644
index 00000000000..daf913b1b34
--- /dev/null
+++ b/vendor/gitignore/Go.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/gitignore/Gradle.gitignore b/vendor/gitignore/Gradle.gitignore
new file mode 100644
index 00000000000..a1fc39c070f
--- /dev/null
+++ b/vendor/gitignore/Gradle.gitignore
@@ -0,0 +1,14 @@
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
diff --git a/vendor/gitignore/Grails.gitignore b/vendor/gitignore/Grails.gitignore
new file mode 100644
index 00000000000..9185f14c37c
--- /dev/null
+++ b/vendor/gitignore/Grails.gitignore
@@ -0,0 +1,33 @@
+# .gitignore for Grails 1.2 and 1.3
+# Although this should work for most versions of grails, it is
+# suggested that you use the "grails integrate-with --git" command
+# to generate your .gitignore file.
+
+# web application files
+/web-app/WEB-INF/classes
+
+# default HSQL database files for production mode
+/prodDb.*
+
+# general HSQL database files
+*Db.properties
+*Db.script
+
+# logs
+/stacktrace.log
+/test/reports
+/logs
+
+# project release file
+/*.war
+
+# plugin release files
+/*.zip
+/plugin.xml
+
+# older plugin install locations
+/plugins
+/web-app/plugins
+
+# "temporary" build files
+/target
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
new file mode 100644
index 00000000000..a4ee41ab62b
--- /dev/null
+++ b/vendor/gitignore/Haskell.gitignore
@@ -0,0 +1,19 @@
+dist
+dist-*
+cabal-dev
+*.o
+*.hi
+*.chi
+*.chs.h
+*.dyn_o
+*.dyn_hi
+.hpc
+.hsenv
+.cabal-sandbox/
+cabal.sandbox.config
+*.prof
+*.aux
+*.hp
+*.eventlog
+.stack-work/
+cabal.project.local
diff --git a/vendor/gitignore/IGORPro.gitignore b/vendor/gitignore/IGORPro.gitignore
new file mode 100644
index 00000000000..c62be650036
--- /dev/null
+++ b/vendor/gitignore/IGORPro.gitignore
@@ -0,0 +1,5 @@
+# Avoid including Experiment files: they can be created and edited locally to test the ipf files
+*.pxp
+*.pxt
+*.uxp
+*.uxt
diff --git a/vendor/gitignore/Idris.gitignore b/vendor/gitignore/Idris.gitignore
new file mode 100644
index 00000000000..c28bc7cc675
--- /dev/null
+++ b/vendor/gitignore/Idris.gitignore
@@ -0,0 +1,2 @@
+*.ibc
+*.o
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
new file mode 100644
index 00000000000..32858aad3c3
--- /dev/null
+++ b/vendor/gitignore/Java.gitignore
@@ -0,0 +1,12 @@
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/vendor/gitignore/Jboss.gitignore b/vendor/gitignore/Jboss.gitignore
new file mode 100644
index 00000000000..75d1731ed97
--- /dev/null
+++ b/vendor/gitignore/Jboss.gitignore
@@ -0,0 +1,19 @@
+jboss/server/all/deploy/project.ext
+jboss/server/default/deploy/project.ext
+jboss/server/minimal/deploy/project.ext
+jboss/server/all/log/*.log
+jboss/server/all/tmp/**/*
+jboss/server/all/data/**/*
+jboss/server/all/work/**/*
+jboss/server/default/log/*.log
+jboss/server/default/tmp/**/*
+jboss/server/default/data/**/*
+jboss/server/default/work/**/*
+jboss/server/minimal/log/*.log
+jboss/server/minimal/tmp/**/*
+jboss/server/minimal/data/**/*
+jboss/server/minimal/work/**/*
+
+# deployed package files #
+
+*.DEPLOYED
diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore
new file mode 100644
index 00000000000..5c91b60c063
--- /dev/null
+++ b/vendor/gitignore/Jekyll.gitignore
@@ -0,0 +1,3 @@
+_site/
+.sass-cache/
+.jekyll-metadata
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
new file mode 100644
index 00000000000..0d7a0de298f
--- /dev/null
+++ b/vendor/gitignore/Joomla.gitignore
@@ -0,0 +1,546 @@
+/.gitignore
+/.htaccess
+/administrator/cache/*
+/administrator/components/com_admin/*
+/administrator/components/com_ajax/*
+/administrator/components/com_tags/*
+/administrator/components/com_banners/*
+/administrator/components/com_cache/*
+/administrator/components/com_postinstall/*
+/administrator/components/com_joomlaupdate/*
+/administrator/components/com_contenthistory/*
+/administrator/components/com_categories/*
+/administrator/components/com_checkin/*
+/administrator/components/com_config/*
+/administrator/components/com_contact/*
+/administrator/components/com_content/*
+/administrator/components/com_cpanel/*
+/administrator/components/com_finder/*
+/administrator/components/com_installer/*
+/administrator/components/com_languages/*
+/administrator/components/com_login/*
+/administrator/components/com_media/*
+/administrator/components/com_menus/*
+/administrator/components/com_messages/*
+/administrator/components/com_modules/*
+/administrator/components/com_newsfeeds/*
+/administrator/components/com_plugins/*
+/administrator/components/com_redirect/*
+/administrator/components/com_search/*
+/administrator/components/com_templates/*
+/administrator/components/com_users/*
+/administrator/components/com_weblinks/*
+/administrator/components/index.html
+/administrator/help/*
+/administrator/includes/*
+/administrator/language/en-GB/en-GB.com_ajax.ini
+/administrator/language/en-GB/en-GB.com_ajax.sys.ini
+/administrator/language/en-GB/en-GB.com_contenthistory.ini
+/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini
+/administrator/language/en-GB/en-GB.com_joomlaupdate.ini
+/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini
+/administrator/language/en-GB/en-GB.com_postinstall.ini
+/administrator/language/en-GB/en-GB.com_postinstall.sys.ini
+/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini
+/administrator/language/en-GB/en-GB.com_tags.ini
+/administrator/language/en-GB/en-GB.com_tags.sys.ini
+/administrator/language/en-GB/en-GB.mod_stats_admin.ini
+/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini
+/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini
+/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_contact.ini
+/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_finder.ini
+/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_categories.ini
+/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_contacts.ini
+/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_content.ini
+/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini
+/administrator/language/en-GB/en-GB.plg_finder_tags.ini
+/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini
+/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini
+/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini
+/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini
+/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini
+/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini
+/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_tags.ini
+/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_languagecode.ini
+/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini
+/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini
+/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini
+/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini
+/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini
+/administrator/language/en-GB/en-GB.tpl_isis.ini
+/administrator/language/en-GB/en-GB.tpl_isis.sys.ini
+/administrator/language/en-GB/install.xml
+/administrator/language/en-GB/en-GB.com_admin.ini
+/administrator/language/en-GB/en-GB.com_admin.sys.ini
+/administrator/language/en-GB/en-GB.com_banners.ini
+/administrator/language/en-GB/en-GB.com_banners.sys.ini
+/administrator/language/en-GB/en-GB.com_cache.ini
+/administrator/language/en-GB/en-GB.com_cache.sys.ini
+/administrator/language/en-GB/en-GB.com_categories.ini
+/administrator/language/en-GB/en-GB.com_categories.sys.ini
+/administrator/language/en-GB/en-GB.com_checkin.ini
+/administrator/language/en-GB/en-GB.com_checkin.sys.ini
+/administrator/language/en-GB/en-GB.com_config.ini
+/administrator/language/en-GB/en-GB.com_config.sys.ini
+/administrator/language/en-GB/en-GB.com_contact.ini
+/administrator/language/en-GB/en-GB.com_contact.sys.ini
+/administrator/language/en-GB/en-GB.com_content.ini
+/administrator/language/en-GB/en-GB.com_content.sys.ini
+/administrator/language/en-GB/en-GB.com_cpanel.ini
+/administrator/language/en-GB/en-GB.com_cpanel.sys.ini
+/administrator/language/en-GB/en-GB.com_finder.ini
+/administrator/language/en-GB/en-GB.com_finder.sys.ini
+/administrator/language/en-GB/en-GB.com_installer.ini
+/administrator/language/en-GB/en-GB.com_installer.sys.ini
+/administrator/language/en-GB/en-GB.com_languages.ini
+/administrator/language/en-GB/en-GB.com_languages.sys.ini
+/administrator/language/en-GB/en-GB.com_login.ini
+/administrator/language/en-GB/en-GB.com_login.sys.ini
+/administrator/language/en-GB/en-GB.com_mailto.sys.ini
+/administrator/language/en-GB/en-GB.com_media.ini
+/administrator/language/en-GB/en-GB.com_media.sys.ini
+/administrator/language/en-GB/en-GB.com_menus.ini
+/administrator/language/en-GB/en-GB.com_menus.sys.ini
+/administrator/language/en-GB/en-GB.com_messages.ini
+/administrator/language/en-GB/en-GB.com_messages.sys.ini
+/administrator/language/en-GB/en-GB.com_modules.ini
+/administrator/language/en-GB/en-GB.com_modules.sys.ini
+/administrator/language/en-GB/en-GB.com_newsfeeds.ini
+/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini
+/administrator/language/en-GB/en-GB.com_plugins.ini
+/administrator/language/en-GB/en-GB.com_plugins.sys.ini
+/administrator/language/en-GB/en-GB.com_redirect.ini
+/administrator/language/en-GB/en-GB.com_redirect.sys.ini
+/administrator/language/en-GB/en-GB.com_search.ini
+/administrator/language/en-GB/en-GB.com_search.sys.ini
+/administrator/language/en-GB/en-GB.com_templates.ini
+/administrator/language/en-GB/en-GB.com_templates.sys.ini
+/administrator/language/en-GB/en-GB.com_users.ini
+/administrator/language/en-GB/en-GB.com_users.sys.ini
+/administrator/language/en-GB/en-GB.com_weblinks.ini
+/administrator/language/en-GB/en-GB.com_weblinks.sys.ini
+/administrator/language/en-GB/en-GB.com_wrapper.ini
+/administrator/language/en-GB/en-GB.com_wrapper.sys.ini
+/administrator/language/en-GB/en-GB.ini
+/administrator/language/en-GB/en-GB.lib_joomla.ini
+/administrator/language/en-GB/en-GB.localise.php
+/administrator/language/en-GB/en-GB.mod_custom.ini
+/administrator/language/en-GB/en-GB.mod_custom.sys.ini
+/administrator/language/en-GB/en-GB.mod_feed.ini
+/administrator/language/en-GB/en-GB.mod_feed.sys.ini
+/administrator/language/en-GB/en-GB.mod_latest.ini
+/administrator/language/en-GB/en-GB.mod_latest.sys.ini
+/administrator/language/en-GB/en-GB.mod_logged.ini
+/administrator/language/en-GB/en-GB.mod_logged.sys.ini
+/administrator/language/en-GB/en-GB.mod_login.ini
+/administrator/language/en-GB/en-GB.mod_login.sys.ini
+/administrator/language/en-GB/en-GB.mod_menu.ini
+/administrator/language/en-GB/en-GB.mod_menu.sys.ini
+/administrator/language/en-GB/en-GB.mod_multilangstatus.ini
+/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini
+/administrator/language/en-GB/en-GB.mod_online.ini
+/administrator/language/en-GB/en-GB.mod_online.sys.ini
+/administrator/language/en-GB/en-GB.mod_popular.ini
+/administrator/language/en-GB/en-GB.mod_popular.sys.ini
+/administrator/language/en-GB/en-GB.mod_quickicon.ini
+/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini
+/administrator/language/en-GB/en-GB.mod_status.ini
+/administrator/language/en-GB/en-GB.mod_status.sys.ini
+/administrator/language/en-GB/en-GB.mod_submenu.ini
+/administrator/language/en-GB/en-GB.mod_submenu.sys.ini
+/administrator/language/en-GB/en-GB.mod_title.ini
+/administrator/language/en-GB/en-GB.mod_title.sys.ini
+/administrator/language/en-GB/en-GB.mod_toolbar.ini
+/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini
+/administrator/language/en-GB/en-GB.mod_unread.ini
+/administrator/language/en-GB/en-GB.mod_unread.sys.ini
+/administrator/language/en-GB/en-GB.mod_version.ini
+/administrator/language/en-GB/en-GB.mod_version.sys.ini
+/administrator/language/en-GB/en-GB.plg_authentication_example.ini
+/administrator/language/en-GB/en-GB.plg_authentication_example.sys.ini
+/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini
+/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini
+/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini
+/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini
+/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini
+/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini
+/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini
+/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini
+/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_geshi.ini
+/administrator/language/en-GB/en-GB.plg_content_geshi.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_joomla.ini
+/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini
+/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini
+/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini
+/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini
+/administrator/language/en-GB/en-GB.plg_content_vote.ini
+/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini
+/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors_none.ini
+/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini
+/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini
+/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini
+/administrator/language/en-GB/en-GB.plg_extension_joomla.ini
+/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini
+/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini
+/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_categories.ini
+/administrator/language/en-GB/en-GB.plg_search_categories.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_contacts.ini
+/administrator/language/en-GB/en-GB.plg_search_contacts.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_content.ini
+/administrator/language/en-GB/en-GB.plg_search_content.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_newsfeeds.ini
+/administrator/language/en-GB/en-GB.plg_search_newsfeeds.sys.ini
+/administrator/language/en-GB/en-GB.plg_search_weblinks.ini
+/administrator/language/en-GB/en-GB.plg_search_weblinks.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_cache.ini
+/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_debug.ini
+/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_highlight.ini
+/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini
+/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_log.ini
+/administrator/language/en-GB/en-GB.plg_system_logout.ini
+/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_log.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_p3p.ini
+/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_redirect.ini
+/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_remember.ini
+/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini
+/administrator/language/en-GB/en-GB.plg_system_sef.ini
+/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini
+/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini
+/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini
+/administrator/language/en-GB/en-GB.plg_user_joomla.ini
+/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini
+/administrator/language/en-GB/en-GB.plg_user_profile.ini
+/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini
+/administrator/language/en-GB/en-GB.tpl_bluestork.ini
+/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini
+/administrator/language/en-GB/en-GB.tpl_hathor.ini
+/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
+/administrator/language/en-GB/en-GB.xml
+/administrator/language/en-GB/index.html
+/administrator/language/overrides/*
+/administrator/language/index.html
+/administrator/manifests/*
+/administrator/modules/mod_custom/*
+/administrator/modules/mod_feed/*
+/administrator/modules/mod_latest/*
+/administrator/modules/mod_logged/*
+/administrator/modules/mod_login/*
+/administrator/modules/mod_menu/*
+/administrator/modules/mod_multilangstatus/*
+/administrator/modules/mod_online/*
+/administrator/modules/mod_popular/*
+/administrator/modules/mod_quickicon/*
+/administrator/modules/mod_status/*
+/administrator/modules/mod_submenu/*
+/administrator/modules/mod_title/*
+/administrator/modules/mod_toolbar/*
+/administrator/modules/mod_unread/*
+/administrator/modules/mod_version/*
+/administrator/modules/mod_stats_admin/*
+/administrator/modules/index.html
+/administrator/templates/bluestork/*
+/administrator/templates/isis/*
+/administrator/templates/hathor/*
+/administrator/templates/system/*
+/administrator/templates/index.html
+/administrator/index.php
+/cache/*
+/bin/*
+/cli/*
+/components/com_banners/*
+/components/com_ajax/*
+/components/com_config/*
+/components/com_contenthistory/*
+/components/com_tags/*
+/components/com_contact/*
+/components/com_content/*
+/components/com_finder/*
+/components/com_mailto/*
+/components/com_media/*
+/components/com_newsfeeds/*
+/components/com_search/*
+/components/com_users/*
+/components/com_weblinks/*
+/components/com_wrapper/*
+/components/index.html
+/images/banners/*
+/images/headers/*
+/images/sampledata/*
+/images/joomla*
+/images/index.html
+/images/powered_by.png
+/includes/*
+/installation/*
+/language/en-GB/en-GB.com_ajax.ini
+/language/en-GB/en-GB.com_config.ini
+/language/en-GB/en-GB.com_contact.ini
+/language/en-GB/en-GB.com_finder.ini
+/language/en-GB/en-GB.com_tags.ini
+/language/en-GB/en-GB.finder_cli.ini
+/language/en-GB/en-GB.lib_fof.sys.ini
+/language/en-GB/en-GB.lib_fof.ini
+/language/en-GB/en-GB.com_content.ini
+/language/en-GB/en-GB.lib_idna_convert.sys.ini
+/language/en-GB/en-GB.com_mailto.ini
+/language/en-GB/en-GB.lib_joomla.sys.ini
+/language/en-GB/en-GB.lib_phpass.sys.ini
+/language/en-GB/en-GB.lib_phpmailer.sys.ini
+/language/en-GB/en-GB.lib_phputf8.sys.ini
+/language/en-GB/en-GB.lib_simplepie.sys.ini
+/language/en-GB/en-GB.com_media.ini
+/language/en-GB/en-GB.mod_finder.ini
+/language/en-GB/en-GB.com_messages.ini
+/language/en-GB/en-GB.mod_tags_popular.ini
+/language/en-GB/en-GB.mod_tags_popular.sys.ini
+/language/en-GB/en-GB.mod_tags_similar.ini
+/language/en-GB/en-GB.mod_tags_similar.sys.ini
+/language/en-GB/en-GB.mod_finder.sys.ini
+/language/en-GB/en-GB.tpl_beez3.ini
+/language/en-GB/en-GB.tpl_beez3.sys.ini
+/language/en-GB/en-GB.com_newsfeeds.ini
+/language/en-GB/en-GB.tpl_protostar.ini
+/language/en-GB/en-GB.tpl_protostar.sys.ini
+/language/en-GB/en-GB.com_search.ini
+/language/en-GB/en-GB.com_users.ini
+/language/en-GB/en-GB.com_weblinks.ini
+/language/en-GB/en-GB.com_wrapper.ini
+/language/en-GB/en-GB.files_joomla.sys.ini
+/language/en-GB/en-GB.ini
+/language/en-GB/en-GB.lib_joomla.ini
+/language/en-GB/en-GB.localise.php
+/language/en-GB/en-GB.mod_articles_archive.ini
+/language/en-GB/en-GB.mod_articles_archive.sys.ini
+/language/en-GB/en-GB.mod_articles_categories.ini
+/language/en-GB/en-GB.mod_articles_categories.sys.ini
+/language/en-GB/en-GB.mod_articles_category.ini
+/language/en-GB/en-GB.mod_articles_category.sys.ini
+/language/en-GB/en-GB.mod_articles_latest.ini
+/language/en-GB/en-GB.mod_articles_latest.sys.ini
+/language/en-GB/en-GB.mod_articles_news.ini
+/language/en-GB/en-GB.mod_articles_news.sys.ini
+/language/en-GB/en-GB.mod_articles_popular.ini
+/language/en-GB/en-GB.mod_articles_popular.sys.ini
+/language/en-GB/en-GB.mod_banners.ini
+/language/en-GB/en-GB.mod_banners.sys.ini
+/language/en-GB/en-GB.mod_breadcrumbs.ini
+/language/en-GB/en-GB.mod_breadcrumbs.sys.ini
+/language/en-GB/en-GB.mod_custom.ini
+/language/en-GB/en-GB.mod_custom.sys.ini
+/language/en-GB/en-GB.mod_feed.ini
+/language/en-GB/en-GB.mod_feed.sys.ini
+/language/en-GB/en-GB.mod_footer.ini
+/language/en-GB/en-GB.mod_footer.sys.ini
+/language/en-GB/en-GB.mod_languages.ini
+/language/en-GB/en-GB.mod_languages.sys.ini
+/language/en-GB/en-GB.mod_login.ini
+/language/en-GB/en-GB.mod_login.sys.ini
+/language/en-GB/en-GB.mod_menu.ini
+/language/en-GB/en-GB.mod_menu.sys.ini
+/language/en-GB/en-GB.mod_random_image.ini
+/language/en-GB/en-GB.mod_random_image.sys.ini
+/language/en-GB/en-GB.mod_related_items.ini
+/language/en-GB/en-GB.mod_related_items.sys.ini
+/language/en-GB/en-GB.mod_search.ini
+/language/en-GB/en-GB.mod_search.sys.ini
+/language/en-GB/en-GB.mod_stats.ini
+/language/en-GB/en-GB.mod_stats.sys.ini
+/language/en-GB/en-GB.mod_syndicate.ini
+/language/en-GB/en-GB.mod_syndicate.sys.ini
+/language/en-GB/en-GB.mod_users_latest.ini
+/language/en-GB/en-GB.mod_users_latest.sys.ini
+/language/en-GB/en-GB.mod_weblinks.ini
+/language/en-GB/en-GB.mod_weblinks.sys.ini
+/language/en-GB/en-GB.mod_whosonline.ini
+/language/en-GB/en-GB.mod_whosonline.sys.ini
+/language/en-GB/en-GB.mod_wrapper.ini
+/language/en-GB/en-GB.mod_wrapper.sys.ini
+/language/en-GB/en-GB.tpl_atomic.ini
+/language/en-GB/en-GB.tpl_atomic.sys.ini
+/language/en-GB/en-GB.tpl_beez_20.ini
+/language/en-GB/en-GB.tpl_beez_20.sys.ini
+/language/en-GB/en-GB.tpl_beez5.ini
+/language/en-GB/en-GB.tpl_beez5.sys.ini
+/language/en-GB/en-GB.xml
+/language/en-GB/index.html
+/language/en-GB/install.xml
+/language/overrides/*
+/language/index.html
+/layouts/joomla/*
+/layouts/libraries/*
+/layouts/plugins/*
+/layouts/index.html
+/libraries/cms.php
+/libraries/cms/*
+/libraries/fof/*
+/libraries/idna_convert/*
+/libraries/joomla/*
+/libraries/legacy/*
+/libraries/phpass/*
+/libraries/phpmailer/*
+/libraries/phputf8/*
+/libraries/simplepie/*
+/libraries/vendor/*
+/libraries/classmap.php
+/libraries/import.legacy.php
+/libraries/index.html
+/libraries/import.php
+/libraries/loader.php
+/libraries/platform.php
+/logs/*
+/media/cms/*
+/media/com_contenthistory/*
+/media/com_finder/*
+/media/com_joomlaupdate/*
+/media/com_wrapper/*
+/media/contacts/*
+/media/editors/*
+/media/jui/*
+/media/mailto/*
+/media/media/*
+/media/mod_languages/*
+/media/overrider/*
+/media/plg_quickicon_extensionupdate/*
+/media/plg_quickicon_joomlaupdate/*
+/media/plg_system_highlight/*
+/media/system/*
+/media/index.html
+/modules/mod_articles_archive/*
+/modules/mod_articles_categories/*
+/modules/mod_articles_category/*
+/modules/mod_articles_latest/*
+/modules/mod_articles_news/*
+/modules/mod_articles_popular/*
+/modules/mod_banners/*
+/modules/mod_breadcrumbs/*
+/modules/mod_custom/*
+/modules/mod_feed/*
+/modules/mod_finder/*
+/modules/mod_footer/*
+/modules/mod_languages/*
+/modules/mod_login/*
+/modules/mod_menu/*
+/modules/mod_random_image/*
+/modules/mod_related_items/*
+/modules/mod_search/*
+/modules/mod_stats/*
+/modules/mod_syndicate/*
+/modules/mod_tags_popular/*
+/modules/mod_tags_similar/*
+/modules/mod_users_latest/*
+/modules/mod_weblinks/*
+/modules/mod_whosonline/*
+/modules/mod_wrapper/*
+/modules/index.html
+/plugins/authentication/example/*
+/plugins/authentication/gmail/*
+/plugins/authentication/joomla/*
+/plugins/authentication/ldap/*
+/plugins/authentication/cookie/*
+/plugins/authentication/index.html
+/plugins/captcha/recaptcha/*
+/plugins/captcha/index.html
+/plugins/content/emailcloak/*
+/plugins/content/example/*
+/plugins/content/finder/*
+/plugins/content/geshi/*
+/plugins/content/joomla/*
+/plugins/content/loadmodule/*
+/plugins/content/pagebreak/*
+/plugins/content/pagenavigation/*
+/plugins/content/vote/*
+/plugins/content/contact/*
+/plugins/content/index.html
+/plugins/editors/codemirror/*
+/plugins/editors/none/*
+/plugins/editors/tinymce/*
+/plugins/editors/index.html
+/plugins/editors-xtd/article/*
+/plugins/editors-xtd/image/*
+/plugins/editors-xtd/pagebreak/*
+/plugins/editors-xtd/readmore/*
+/plugins/editors-xtd/index.html
+/plugins/extension/example/*
+/plugins/extension/joomla/*
+/plugins/extension/index.html
+/plugins/finder/index.html
+/plugins/finder/categories/*
+/plugins/finder/contacts/*
+/plugins/finder/content/*
+/plugins/finder/newsfeeds/*
+/plugins/finder/tags/*
+/plugins/finder/weblinks/*
+/plugins/installer/*
+/plugins/quickicon/extensionupdate/*
+/plugins/quickicon/joomlaupdate/*
+/plugins/quickicon/index.html
+/plugins/search/categories/*
+/plugins/search/contacts/*
+/plugins/search/content/*
+/plugins/search/newsfeeds/*
+/plugins/search/weblinks/*
+/plugins/search/tags/*
+/plugins/search/index.html
+/plugins/system/cache/*
+/plugins/system/debug/*
+/plugins/system/highlight/*
+/plugins/system/languagecode/*
+/plugins/system/languagefilter/*
+/plugins/system/log/*
+/plugins/system/logout/*
+/plugins/system/p3p/*
+/plugins/system/redirect/*
+/plugins/system/remember/*
+/plugins/system/sef/*
+/plugins/system/index.html
+/plugins/twofactorauth/*
+/plugins/user/contactcreator/*
+/plugins/user/example/*
+/plugins/user/joomla/*
+/plugins/user/profile/*
+/plugins/user/index.html
+/plugins/index.html
+/templates/atomic/*
+/templates/beez3/*
+/templates/beez_20/*
+/templates/beez5/*
+/templates/protostar/*
+/templates/system/*
+/templates/index.html
+/tmp/*
+/configuration.php
+/index.php
+/joomla.xml
+/*.txt
+/robots.txt.dist
diff --git a/vendor/gitignore/Julia.gitignore b/vendor/gitignore/Julia.gitignore
new file mode 100644
index 00000000000..381e0b6d252
--- /dev/null
+++ b/vendor/gitignore/Julia.gitignore
@@ -0,0 +1,4 @@
+*.jl.cov
+*.jl.*.cov
+*.jl.mem
+deps/deps.jl
diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore
new file mode 100644
index 00000000000..606ed1c7b4d
--- /dev/null
+++ b/vendor/gitignore/KiCad.gitignore
@@ -0,0 +1,20 @@
+# For PCBs designed using KiCad: http://www.kicad-pcb.org/
+
+# Temporary files
+*.000
+*.bak
+*.bck
+*.kicad_pcb-bak
+*~
+_autosave-*
+*.tmp
+
+# Netlist files (exported from Eeschema)
+*.net
+
+# Autorouter files (exported from Pcbnew)
+.dsn
+
+# Exported BOM files
+*.xml
+*.csv
diff --git a/vendor/gitignore/Kohana.gitignore b/vendor/gitignore/Kohana.gitignore
new file mode 100644
index 00000000000..8b2ab01a800
--- /dev/null
+++ b/vendor/gitignore/Kohana.gitignore
@@ -0,0 +1,2 @@
+application/cache/*
+application/logs/*
diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE
new file mode 100644
index 00000000000..0e259d42c99
--- /dev/null
+++ b/vendor/gitignore/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/vendor/gitignore/LabVIEW.gitignore b/vendor/gitignore/LabVIEW.gitignore
new file mode 100644
index 00000000000..122450865cf
--- /dev/null
+++ b/vendor/gitignore/LabVIEW.gitignore
@@ -0,0 +1,16 @@
+# Libraries
+*.lvlibp
+*.llb
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+
+# Metadata
+*.aliases
+*.lvlps
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
new file mode 100644
index 00000000000..1cd717b6921
--- /dev/null
+++ b/vendor/gitignore/Laravel.gitignore
@@ -0,0 +1,15 @@
+vendor/
+node_modules/
+
+# Laravel 4 specific
+bootstrap/compiled.php
+app/storage/
+
+# Laravel 5 & Lumen specific
+bootstrap/cache/
+.env.*.php
+.env.php
+.env
+
+# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
+.rocketeer/
diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore
new file mode 100644
index 00000000000..47fed6c20d9
--- /dev/null
+++ b/vendor/gitignore/Leiningen.gitignore
@@ -0,0 +1,12 @@
+pom.xml
+pom.xml.asc
+*jar
+/lib/
+/classes/
+/target/
+/checkouts/
+.lein-deps-sum
+.lein-repl-history
+.lein-plugins/
+.lein-failures
+.nrepl-port
diff --git a/vendor/gitignore/LemonStand.gitignore b/vendor/gitignore/LemonStand.gitignore
new file mode 100644
index 00000000000..c7d94ad34b0
--- /dev/null
+++ b/vendor/gitignore/LemonStand.gitignore
@@ -0,0 +1,21 @@
+boot.php
+index.php
+install.php
+/config/*
+!/config/config.php
+/controllers/*
+/init/*
+/logs/*
+/phproad/*
+/temp/*
+/uploaded/*
+/installer_files/*
+/modules/backend/*
+/modules/blog/*
+/modules/cms/*
+/modules/core/*
+/modules/session/*
+/modules/shop/*
+/modules/system/*
+/modules/users/*
+# add content_*.php if you don't want erase client changes to content
diff --git a/vendor/gitignore/Lilypond.gitignore b/vendor/gitignore/Lilypond.gitignore
new file mode 100644
index 00000000000..513e6edd9c4
--- /dev/null
+++ b/vendor/gitignore/Lilypond.gitignore
@@ -0,0 +1,6 @@
+*.pdf
+*.ps
+*.midi
+*.mid
+*.log
+*~
diff --git a/vendor/gitignore/Lithium.gitignore b/vendor/gitignore/Lithium.gitignore
new file mode 100644
index 00000000000..7b22568ea89
--- /dev/null
+++ b/vendor/gitignore/Lithium.gitignore
@@ -0,0 +1,2 @@
+libraries/*
+resources/tmp/*
diff --git a/vendor/gitignore/Lua.gitignore b/vendor/gitignore/Lua.gitignore
new file mode 100644
index 00000000000..6fd0a376dec
--- /dev/null
+++ b/vendor/gitignore/Lua.gitignore
@@ -0,0 +1,41 @@
+# Compiled Lua sources
+luac.out
+
+# luarocks build files
+*.src.rock
+*.zip
+*.tar.gz
+
+# Object files
+*.o
+*.os
+*.ko
+*.obj
+*.elf
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+*.def
+*.exp
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
new file mode 100644
index 00000000000..195c9b7a029
--- /dev/null
+++ b/vendor/gitignore/Magento.gitignore
@@ -0,0 +1,104 @@
+.htaccess.sample
+.modgit/
+.modman/
+app/code/community/Phoenix/Moneybookers/
+app/code/community/Cm/RedisSession/
+app/code/core/
+app/design/adminhtml/default/default/
+app/design/frontend/base/
+app/design/frontend/rwd/
+app/design/frontend/default/blank/
+app/design/frontend/default/default/
+app/design/frontend/default/iphone/
+app/design/frontend/default/modern/
+app/design/frontend/enterprise/default
+app/design/install/
+app/etc/modules/Enterprise_*
+app/etc/modules/Mage_*.xml
+app/etc/modules/Phoenix_Moneybookers.xml
+app/etc/modules/Cm_RedisSession.xml
+app/etc/applied.patches.list
+app/etc/config.xml
+app/etc/enterprise.xml
+app/etc/local.xml.additional
+app/etc/local.xml.template
+app/etc/local.xml
+app/.htaccess
+app/bootstrap.php
+app/locale/en_US/
+app/Mage.php
+/cron.php
+cron.sh
+dev/.htaccess
+dev/tests/functional/
+downloader/
+errors/
+favicon.ico
+/get.php
+includes/
+/index.php
+index.php.sample
+/install.php
+js/blank.html
+js/calendar/
+js/enterprise/
+js/extjs/
+js/firebug/
+js/flash/
+js/index.php
+js/jscolor/
+js/lib/
+js/mage/
+js/prototype/
+js/scriptaculous/
+js/spacer.gif
+js/tiny_mce/
+js/varien/
+lib/3Dsecure/
+lib/Apache/
+lib/flex/
+lib/googlecheckout/
+lib/.htaccess
+lib/LinLibertineFont/
+lib/Mage/
+lib/PEAR/
+lib/Pelago/
+lib/phpseclib/
+lib/Varien/
+lib/Zend/
+lib/Cm/
+lib/Credis/
+lib/Magento/
+LICENSE_AFL.txt
+LICENSE.html
+LICENSE.txt
+LICENSE_EE*
+/mage
+media/
+/api.php
+nbproject/
+pear
+pear/
+php.ini.sample
+pkginfo/
+RELEASE_NOTES.txt
+shell/.htaccess
+shell/abstract.php
+shell/compiler.php
+shell/indexer.php
+shell/log.php
+sitemap.xml
+skin/adminhtml/default/default/
+skin/adminhtml/default/enterprise
+skin/frontend/base/
+skin/frontend/rwd/
+skin/frontend/default/blank/
+skin/frontend/default/blue/
+skin/frontend/default/default/
+skin/frontend/default/french/
+skin/frontend/default/german/
+skin/frontend/default/iphone/
+skin/frontend/default/modern/
+skin/frontend/enterprise
+skin/install/
+var/
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
new file mode 100644
index 00000000000..1cdc9f7fd45
--- /dev/null
+++ b/vendor/gitignore/Maven.gitignore
@@ -0,0 +1,9 @@
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
diff --git a/vendor/gitignore/Mercury.gitignore b/vendor/gitignore/Mercury.gitignore
new file mode 100644
index 00000000000..70ec8693971
--- /dev/null
+++ b/vendor/gitignore/Mercury.gitignore
@@ -0,0 +1,13 @@
+Mercury/
+Mercury.modules
+*.mh
+*.err
+*.init
+*.dll
+*.exe
+*.a
+*.so
+*.dylib
+*.beams
+*.d
+*.c_date
diff --git a/vendor/gitignore/MetaProgrammingSystem.gitignore b/vendor/gitignore/MetaProgrammingSystem.gitignore
new file mode 100644
index 00000000000..3e75841041c
--- /dev/null
+++ b/vendor/gitignore/MetaProgrammingSystem.gitignore
@@ -0,0 +1,16 @@
+workspace.xml
+junitvmwatcher*.properties
+build.properties
+
+# generated java classes and java source files
+# manually add any custom artifacts that can't be generated from the models
+# http://confluence.jetbrains.com/display/MPSD25/HowTo+--+MPS+and+Git
+classes_gen
+source_gen
+source_gen.caches
+
+# generated test code and test results
+test_gen
+test_gen.caches
+TEST-*.xml
+junit*.properties
diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore
new file mode 100644
index 00000000000..abc21828a3e
--- /dev/null
+++ b/vendor/gitignore/Nanoc.gitignore
@@ -0,0 +1,10 @@
+# For projects using nanoc (http://nanoc.ws/)
+
+# Default location for output, needs to match output_dir's value found in config.yaml
+output/
+
+# Temporary file directory
+tmp/
+
+# Crash Log
+crash.log
diff --git a/vendor/gitignore/Nim.gitignore b/vendor/gitignore/Nim.gitignore
new file mode 100644
index 00000000000..67d9b34c6ce
--- /dev/null
+++ b/vendor/gitignore/Nim.gitignore
@@ -0,0 +1 @@
+nimcache/
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
new file mode 100644
index 00000000000..aea5294de9d
--- /dev/null
+++ b/vendor/gitignore/Node.gitignore
@@ -0,0 +1,38 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules
+jspm_packages
+
+# Optional npm cache directory
+.npm
+
+# Optional REPL history
+.node_repl_history
diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore
new file mode 100644
index 00000000000..f7817ae5c36
--- /dev/null
+++ b/vendor/gitignore/OCaml.gitignore
@@ -0,0 +1,20 @@
+*.annot
+*.cmo
+*.cma
+*.cmi
+*.a
+*.o
+*.cmx
+*.cmxs
+*.cmxa
+
+# ocamlbuild working directory
+_build/
+
+# ocamlbuild targets
+*.byte
+*.native
+
+# oasis generated files
+setup.data
+setup.log
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
new file mode 100644
index 00000000000..86f21d8e0ff
--- /dev/null
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -0,0 +1,60 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xcuserstate
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/screenshots
+
+#Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
diff --git a/vendor/gitignore/Opa.gitignore b/vendor/gitignore/Opa.gitignore
new file mode 100644
index 00000000000..74c6219ceda
--- /dev/null
+++ b/vendor/gitignore/Opa.gitignore
@@ -0,0 +1,13 @@
+_build
+_tracks
+
+opa-debug-js
+
+*.opp
+*.opx
+*.opx.broken
+*.dump
+*.api
+*.api-txt
+*.exe
+*.log
diff --git a/vendor/gitignore/OpenCart.gitignore b/vendor/gitignore/OpenCart.gitignore
new file mode 100644
index 00000000000..28e45aa6aac
--- /dev/null
+++ b/vendor/gitignore/OpenCart.gitignore
@@ -0,0 +1,13 @@
+.htaccess
+/config.php
+admin/config.php
+
+!index.html
+
+download/
+image/data/
+image/cache/
+system/cache/
+system/logs/
+
+system/storage/
diff --git a/vendor/gitignore/OracleForms.gitignore b/vendor/gitignore/OracleForms.gitignore
new file mode 100644
index 00000000000..699a4940118
--- /dev/null
+++ b/vendor/gitignore/OracleForms.gitignore
@@ -0,0 +1,8 @@
+# Compiled Form Modules
+*.fmx
+
+# Compiled Menu Modules
+*.mmx
+
+# Compiled Pre-Linked Libraries
+*.plx
diff --git a/vendor/gitignore/Packer.gitignore b/vendor/gitignore/Packer.gitignore
new file mode 100644
index 00000000000..1b7a03efdd7
--- /dev/null
+++ b/vendor/gitignore/Packer.gitignore
@@ -0,0 +1,5 @@
+# Cache objects
+packer_cache/
+
+# For built boxes
+*.box
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
new file mode 100644
index 00000000000..ae2ad536abb
--- /dev/null
+++ b/vendor/gitignore/Perl.gitignore
@@ -0,0 +1,20 @@
+/blib/
+/.build/
+_build/
+cover_db/
+inc/
+Build
+!Build/
+Build.bat
+.last_cover_stats
+/Makefile
+/Makefile.old
+/MANIFEST.bak
+/META.yml
+/META.json
+/MYMETA.*
+nytprof.out
+/pm_to_blib
+*.o
+*.bs
+/_eumm/
diff --git a/vendor/gitignore/Phalcon.gitignore b/vendor/gitignore/Phalcon.gitignore
new file mode 100644
index 00000000000..6ffe3aa220a
--- /dev/null
+++ b/vendor/gitignore/Phalcon.gitignore
@@ -0,0 +1,2 @@
+/cache/
+/config/development/
diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore
new file mode 100644
index 00000000000..6d67f119175
--- /dev/null
+++ b/vendor/gitignore/PlayFramework.gitignore
@@ -0,0 +1,15 @@
+# Ignore Play! working directory #
+bin/
+/db
+.eclipse
+/lib/
+/logs/
+/modules
+/project/target
+/target
+tmp/
+test-result
+server.pid
+*.eml
+/dist/
+.cache
diff --git a/vendor/gitignore/Plone.gitignore b/vendor/gitignore/Plone.gitignore
new file mode 100644
index 00000000000..770a8681ac3
--- /dev/null
+++ b/vendor/gitignore/Plone.gitignore
@@ -0,0 +1,18 @@
+*.pyc
+*.pyo
+*.tmp*
+*.mo
+*.egg
+*.EGG
+*.egg-info
+*.EGG-INFO
+.*.cfg
+bin/
+build/
+develop-eggs/
+downloads/
+eggs/
+fake-eggs/
+parts/
+dist/
+var/
diff --git a/vendor/gitignore/Prestashop.gitignore b/vendor/gitignore/Prestashop.gitignore
new file mode 100644
index 00000000000..7c6ae1e31cc
--- /dev/null
+++ b/vendor/gitignore/Prestashop.gitignore
@@ -0,0 +1,32 @@
+# Private files
+# The following files contain your database credentials and other personal data.
+
+config/settings.*.php
+
+# Cache, temp and generated files
+# The following files are generated by PrestaShop.
+
+admin-dev/autoupgrade/
+/cache/
+!/cache/index.php
+!/cache/cachefs/index.php
+!/cache/purifier/index.php
+!/cache/push/index.php
+!/cache/sandbox/index.php
+!/cache/smarty/index.php
+!/cache/tcpdf/index.php
+config/xml/*.xml
+/log/*
+*sitemap.xml
+themes/*/cache/
+modules/*/config*.xml
+
+# Site content
+# The following folders contain product images, virtual products, CSV's, etc.
+
+admin-dev/backups/
+admin-dev/export/
+admin-dev/import/
+download/
+/img/*
+upload/
diff --git a/vendor/gitignore/Processing.gitignore b/vendor/gitignore/Processing.gitignore
new file mode 100644
index 00000000000..85f269a89f6
--- /dev/null
+++ b/vendor/gitignore/Processing.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+applet
+application.linux32
+application.linux64
+application.windows32
+application.windows64
+application.macosx
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
new file mode 100644
index 00000000000..72364f99fe4
--- /dev/null
+++ b/vendor/gitignore/Python.gitignore
@@ -0,0 +1,89 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# IPython Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# dotenv
+.env
+
+# virtualenv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+
+# Rope project settings
+.ropeproject
diff --git a/vendor/gitignore/Qooxdoo.gitignore b/vendor/gitignore/Qooxdoo.gitignore
new file mode 100644
index 00000000000..d0c64102d85
--- /dev/null
+++ b/vendor/gitignore/Qooxdoo.gitignore
@@ -0,0 +1,5 @@
+cache
+cache-downloads
+inspector
+api
+source/inspector.html
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
new file mode 100644
index 00000000000..c7659c24f38
--- /dev/null
+++ b/vendor/gitignore/Qt.gitignore
@@ -0,0 +1,38 @@
+# C++ objects and libs
+
+*.slo
+*.lo
+*.o
+*.a
+*.la
+*.lai
+*.so
+*.dll
+*.dylib
+
+# Qt-es
+
+/.qmake.cache
+/.qmake.stash
+*.pro.user
+*.pro.user.*
+*.qbs.user
+*.qbs.user.*
+*.moc
+moc_*.cpp
+qrc_*.cpp
+ui_*.h
+Makefile*
+*build-*
+
+# QtCreator
+
+*.autosave
+
+# QtCtreator Qml
+*.qmlproject.user
+*.qmlproject.user.*
+
+# QtCtreator CMake
+CMakeLists.txt.user*
+
diff --git a/vendor/gitignore/R.gitignore b/vendor/gitignore/R.gitignore
new file mode 100644
index 00000000000..fcff087aebb
--- /dev/null
+++ b/vendor/gitignore/R.gitignore
@@ -0,0 +1,33 @@
+# History files
+.Rhistory
+.Rapp.history
+
+# Session Data files
+.RData
+
+# Example code in package build process
+*-Ex.R
+
+# Output files from R CMD build
+/*.tar.gz
+
+# Output files from R CMD check
+/*.Rcheck/
+
+# RStudio files
+.Rproj.user/
+
+# produced vignettes
+vignettes/*.html
+vignettes/*.pdf
+
+# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
+.httr-oauth
+
+# knitr and R markdown default cache directories
+/*_cache/
+/cache/
+
+# Temporary files created by R markdown
+*.utf8.md
+*.knit.md
diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore
new file mode 100644
index 00000000000..f8bcd117371
--- /dev/null
+++ b/vendor/gitignore/ROS.gitignore
@@ -0,0 +1,47 @@
+build/
+bin/
+lib/
+msg_gen/
+srv_gen/
+msg/*Action.msg
+msg/*ActionFeedback.msg
+msg/*ActionGoal.msg
+msg/*ActionResult.msg
+msg/*Feedback.msg
+msg/*Goal.msg
+msg/*Result.msg
+msg/_*.py
+
+# Generated by dynamic reconfigure
+*.cfgc
+/cfg/cpp/
+/cfg/*.py
+
+# Ignore generated docs
+*.dox
+*.wikidoc
+
+# eclipse stuff
+.project
+.cproject
+
+# qcreator stuff
+CMakeLists.txt.user
+
+srv/_*.py
+*.pcd
+*.pyc
+qtcreator-*
+*.user
+
+/planning/cfg
+/planning/docs
+/planning/src
+
+*~
+
+# Emacs
+.#*
+
+# Catkin custom files
+CATKIN_IGNORE
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
new file mode 100644
index 00000000000..d8c256c1925
--- /dev/null
+++ b/vendor/gitignore/Rails.gitignore
@@ -0,0 +1,42 @@
+*.rbc
+capybara-*.html
+.rspec
+/log
+/tmp
+/db/*.sqlite3
+/db/*.sqlite3-journal
+/public/system
+/coverage/
+/spec/tmp
+**.orig
+rerun.txt
+pickle-email-*.html
+
+# TODO Comment out these rules if you are OK with secrets being uploaded to the repo
+config/initializers/secret_token.rb
+config/secrets.yml
+
+# dotenv
+# TODO Comment out this rule if environment variables can be committed
+.env
+
+## Environment normalization:
+/.bundle
+/vendor/bundle
+
+# these should all be checked in to normalize the environment:
+# Gemfile.lock, .ruby-version, .ruby-gemset
+
+# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
+.rvmrc
+
+# if using bower-rails ignore default bower_components path bower.json files
+/vendor/assets/bower_components
+*.bowerrc
+bower.json
+
+# Ignore pow environment settings
+.powenv
+
+# Ignore Byebug command history file.
+.byebug_history
diff --git a/vendor/gitignore/RhodesRhomobile.gitignore b/vendor/gitignore/RhodesRhomobile.gitignore
new file mode 100644
index 00000000000..a211dcc3b0f
--- /dev/null
+++ b/vendor/gitignore/RhodesRhomobile.gitignore
@@ -0,0 +1,9 @@
+rholog-*
+sim-*
+bin/libs
+bin/RhoBundle
+bin/tmp
+bin/target
+bin/*.ap_
+*.o
+*.jar
diff --git a/vendor/gitignore/Ruby.gitignore b/vendor/gitignore/Ruby.gitignore
new file mode 100644
index 00000000000..5e1422c9c3f
--- /dev/null
+++ b/vendor/gitignore/Ruby.gitignore
@@ -0,0 +1,50 @@
+*.gem
+*.rbc
+/.config
+/coverage/
+/InstalledFiles
+/pkg/
+/spec/reports/
+/spec/examples.txt
+/test/tmp/
+/test/version_tmp/
+/tmp/
+
+# Used by dotenv library to load environment variables.
+# .env
+
+## Specific to RubyMotion:
+.dat*
+.repl_history
+build/
+*.bridgesupport
+build-iPhoneOS/
+build-iPhoneSimulator/
+
+## Specific to RubyMotion (use of CocoaPods):
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# vendor/Pods/
+
+## Documentation cache and generated files:
+/.yardoc/
+/_yardoc/
+/doc/
+/rdoc/
+
+## Environment normalization:
+/.bundle/
+/vendor/bundle
+/lib/bundler/man/
+
+# for a library or gem, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# Gemfile.lock
+# .ruby-version
+# .ruby-gemset
+
+# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
+.rvmrc
diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore
new file mode 100644
index 00000000000..cb14a420640
--- /dev/null
+++ b/vendor/gitignore/Rust.gitignore
@@ -0,0 +1,7 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
+Cargo.lock
diff --git a/vendor/gitignore/SCons.gitignore b/vendor/gitignore/SCons.gitignore
new file mode 100644
index 00000000000..39d9743a082
--- /dev/null
+++ b/vendor/gitignore/SCons.gitignore
@@ -0,0 +1,2 @@
+# for projects that use SCons for building: http://http://www.scons.org/
+.sconsign.dblite
diff --git a/vendor/gitignore/Sass.gitignore b/vendor/gitignore/Sass.gitignore
new file mode 100644
index 00000000000..486b32ce90c
--- /dev/null
+++ b/vendor/gitignore/Sass.gitignore
@@ -0,0 +1,2 @@
+.sass-cache/
+*.css.map
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
new file mode 100644
index 00000000000..c58d83b3189
--- /dev/null
+++ b/vendor/gitignore/Scala.gitignore
@@ -0,0 +1,17 @@
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
diff --git a/vendor/gitignore/Scheme.gitignore b/vendor/gitignore/Scheme.gitignore
new file mode 100644
index 00000000000..cbb89d78da5
--- /dev/null
+++ b/vendor/gitignore/Scheme.gitignore
@@ -0,0 +1,7 @@
+*.ss~
+*.ss#*
+.#*.ss
+
+*.scm~
+*.scm#*
+.#*.scm
diff --git a/vendor/gitignore/Scrivener.gitignore b/vendor/gitignore/Scrivener.gitignore
new file mode 100644
index 00000000000..3b39c66ba12
--- /dev/null
+++ b/vendor/gitignore/Scrivener.gitignore
@@ -0,0 +1,7 @@
+/Files/binder.autosave
+/Files/binder.backup
+/Files/search.indexes
+/Files/user.lock
+/Files/Docs/docs.checksum
+/QuickLook/
+/Settings/ui.plist
diff --git a/vendor/gitignore/Sdcc.gitignore b/vendor/gitignore/Sdcc.gitignore
new file mode 100644
index 00000000000..07ee7d59aba
--- /dev/null
+++ b/vendor/gitignore/Sdcc.gitignore
@@ -0,0 +1,8 @@
+# SDCC stuff
+*.lnk
+*.lst
+*.map
+*.mem
+*.rel
+*.rst
+*.sym
diff --git a/vendor/gitignore/SeamGen.gitignore b/vendor/gitignore/SeamGen.gitignore
new file mode 100644
index 00000000000..a418cf376c5
--- /dev/null
+++ b/vendor/gitignore/SeamGen.gitignore
@@ -0,0 +1,26 @@
+/bootstrap/data
+/bootstrap/tmp
+/classes/
+/dist/
+/exploded-archives/
+/test-build/
+/test-output/
+/test-report/
+/target/
+temp-testng-customsuite.xml
+
+# based on http://stackoverflow.com/a/8865858/422476 I am removing inline comments
+
+#/classes/ all class files
+#/dist/ contains generated war files for deployment
+#/exploded-archives/ war content generation during deploy (or explode)
+#/test-build/ test compilation (ant target for Seam)
+#/test-output/ test results
+#/test-report/ test report generation for, e.g., Hudson
+#/target/ maven output folder
+#temp-testng-customsuite.xml generated when running test cases under Eclipse
+
+# Thanks to @VonC and @kraftan for their helpful answers on a related question
+# on StackOverflow.com:
+# http://stackoverflow.com/questions/4176687
+# /what-is-the-recommended-source-control-ignore-pattern-for-seam-projects
diff --git a/vendor/gitignore/SketchUp.gitignore b/vendor/gitignore/SketchUp.gitignore
new file mode 100644
index 00000000000..5160df3c6bf
--- /dev/null
+++ b/vendor/gitignore/SketchUp.gitignore
@@ -0,0 +1 @@
+*.skb
diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore
new file mode 100644
index 00000000000..75272b23472
--- /dev/null
+++ b/vendor/gitignore/Smalltalk.gitignore
@@ -0,0 +1,18 @@
+# changes file
+*.changes
+
+# system image
+*.image
+
+# Pharo Smalltalk Debug log file
+PharoDebug.log
+
+# Squeak Smalltalk Debug log file
+SqueakDebug.log
+
+# Monticello package cache
+/package-cache
+
+# Metacello-github cache
+/github-cache
+github-*.zip
diff --git a/vendor/gitignore/Stella.gitignore b/vendor/gitignore/Stella.gitignore
new file mode 100644
index 00000000000..402a5438373
--- /dev/null
+++ b/vendor/gitignore/Stella.gitignore
@@ -0,0 +1,12 @@
+# Atari 2600 (Stella) support for multiple assemblers
+# - DASM
+# - CC65
+
+# Assembled binaries and object directories
+obj/
+a.out
+*.bin
+*.a26
+
+# Add in special Atari 7800-based binaries for good measure
+*.a78
diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore
new file mode 100644
index 00000000000..842c3ec518b
--- /dev/null
+++ b/vendor/gitignore/SugarCRM.gitignore
@@ -0,0 +1,25 @@
+## SugarCRM
+# Ignore custom .htaccess stuff.
+/.htaccess
+# Ignore the cache directory completely.
+# This will break the current behaviour. Which was often leading to
+# the misuse of the repository as backup replacement.
+# For development the cache directory can be safely ignored and
+# therefore it is ignored.
+/cache/
+# Ignore some files and directories from the custom directory.
+/custom/history/
+/custom/modulebuilder/
+/custom/working/
+/custom/modules/*/Ext/
+/custom/application/Ext/
+# Custom configuration should also be ignored.
+/config.php
+/config_override.php
+# The silent upgrade scripts aren't needed.
+/silentUpgrade*.php
+# Logs files can safely be ignored.
+*.log
+# Ignore the new upload directories.
+/upload/
+/upload_backup/
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
new file mode 100644
index 00000000000..2c22487b5e3
--- /dev/null
+++ b/vendor/gitignore/Swift.gitignore
@@ -0,0 +1,65 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xcuserstate
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
new file mode 100644
index 00000000000..7d56f982f81
--- /dev/null
+++ b/vendor/gitignore/Symfony.gitignore
@@ -0,0 +1,48 @@
+# Cache and logs (Symfony2)
+/app/cache/*
+/app/logs/*
+!app/cache/.gitkeep
+!app/logs/.gitkeep
+
+# Email spool folder
+/app/spool/*
+
+# Cache, session files and logs (Symfony3)
+/var/cache/*
+/var/logs/*
+/var/sessions/*
+!var/cache/.gitkeep
+!var/logs/.gitkeep
+!var/sessions/.gitkeep
+
+# Parameters
+/app/config/parameters.yml
+/app/config/parameters.ini
+
+# Managed by Composer
+/app/bootstrap.php.cache
+/var/bootstrap.php.cache
+/bin/*
+!bin/console
+!bin/symfony_requirements
+/vendor/
+
+# Assets and user uploads
+/web/bundles/
+/web/uploads/
+
+# Assets managed by Bower
+/web/assets/vendor/
+
+# PHPUnit
+/app/phpunit.xml
+/phpunit.xml
+
+# Build data
+/build/
+
+# Composer PHAR
+/composer.phar
+
+# Backup entities generated with doctrine:generate:entities command
+*/Entity/*~
diff --git a/vendor/gitignore/SymphonyCMS.gitignore b/vendor/gitignore/SymphonyCMS.gitignore
new file mode 100644
index 00000000000..671c7ff9e32
--- /dev/null
+++ b/vendor/gitignore/SymphonyCMS.gitignore
@@ -0,0 +1,6 @@
+manifest/cache/
+manifest/logs/
+manifest/tmp/
+symphony/
+workspace/uploads/
+install-log.txt
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
new file mode 100644
index 00000000000..3cb097c9d5e
--- /dev/null
+++ b/vendor/gitignore/TeX.gitignore
@@ -0,0 +1,183 @@
+## Core latex/pdflatex auxiliary files:
+*.aux
+*.lof
+*.log
+*.lot
+*.fls
+*.out
+*.toc
+*.fmt
+*.fot
+*.cb
+*.cb2
+
+## Intermediate documents:
+*.dvi
+*-converted-to.*
+# these rules might exclude image files for figures etc.
+# *.ps
+# *.eps
+# *.pdf
+
+## Bibliography auxiliary files (bibtex/biblatex/biber):
+*.bbl
+*.bcf
+*.blg
+*-blx.aux
+*-blx.bib
+*.brf
+*.run.xml
+
+## Build tool auxiliary files:
+*.fdb_latexmk
+*.synctex
+*.synctex.gz
+*.synctex.gz(busy)
+*.pdfsync
+
+## Auxiliary and intermediate files from other packages:
+# algorithms
+*.alg
+*.loa
+
+# achemso
+acs-*.bib
+
+# amsthm
+*.thm
+
+# beamer
+*.nav
+*.snm
+*.vrb
+
+# cprotect
+*.cpt
+
+# fixme
+*.lox
+
+#(r)(e)ledmac/(r)(e)ledpar
+*.end
+*.?end
+*.[1-9]
+*.[1-9][0-9]
+*.[1-9][0-9][0-9]
+*.[1-9]R
+*.[1-9][0-9]R
+*.[1-9][0-9][0-9]R
+*.eledsec[1-9]
+*.eledsec[1-9]R
+*.eledsec[1-9][0-9]
+*.eledsec[1-9][0-9]R
+*.eledsec[1-9][0-9][0-9]
+*.eledsec[1-9][0-9][0-9]R
+
+# glossaries
+*.acn
+*.acr
+*.glg
+*.glo
+*.gls
+*.glsdefs
+
+# gnuplottex
+*-gnuplottex-*
+
+# hyperref
+*.brf
+
+# knitr
+*-concordance.tex
+# TODO Comment the next line if you want to keep your tikz graphics files
+*.tikz
+*-tikzDictionary
+
+# listings
+*.lol
+
+# makeidx
+*.idx
+*.ilg
+*.ind
+*.ist
+
+# minitoc
+*.maf
+*.mlf
+*.mlt
+*.mtc
+*.mtc[0-9]
+*.mtc[1-9][0-9]
+
+# minted
+_minted*
+*.pyg
+
+# morewrites
+*.mw
+
+# mylatexformat
+*.fmt
+
+# nomencl
+*.nlo
+
+# sagetex
+*.sagetex.sage
+*.sagetex.py
+*.sagetex.scmd
+
+# sympy
+*.sout
+*.sympy
+sympy-plots-for-*.tex/
+
+# pdfcomment
+*.upa
+*.upb
+
+# pythontex
+*.pytxcode
+pythontex-files-*/
+
+# thmtools
+*.loe
+
+# TikZ & PGF
+*.dpth
+*.md5
+*.auxlock
+
+# todonotes
+*.tdo
+
+# easy-todo
+*.lod
+
+# xindy
+*.xdy
+
+# xypic precompiled matrices
+*.xyc
+
+# endfloat
+*.ttt
+*.fff
+
+# Latexian
+TSWLatexianTemp*
+
+## Editors:
+# WinEdt
+*.bak
+*.sav
+
+# Texpad
+.texpadtmp
+
+# Kile
+*.backup
+
+# KBibTeX
+*~[0-9]*
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
new file mode 100644
index 00000000000..7868d16d216
--- /dev/null
+++ b/vendor/gitignore/Terraform.gitignore
@@ -0,0 +1,3 @@
+# Compiled files
+*.tfstate
+*.tfstate.backup
diff --git a/vendor/gitignore/Textpattern.gitignore b/vendor/gitignore/Textpattern.gitignore
new file mode 100644
index 00000000000..3805636d622
--- /dev/null
+++ b/vendor/gitignore/Textpattern.gitignore
@@ -0,0 +1,11 @@
+.htaccess
+css.php
+rpc/
+sites/site*/admin/
+sites/site*/private/
+sites/site*/public/admin/
+sites/site*/public/setup/
+sites/site*/public/theme/
+textpattern/
+HISTORY.txt
+README.txt
diff --git a/vendor/gitignore/TurboGears2.gitignore b/vendor/gitignore/TurboGears2.gitignore
new file mode 100644
index 00000000000..122b3de221f
--- /dev/null
+++ b/vendor/gitignore/TurboGears2.gitignore
@@ -0,0 +1,20 @@
+*.py[co]
+
+# Default development database
+devdata.db
+
+# Default data directory
+data/*
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
diff --git a/vendor/gitignore/Typo3.gitignore b/vendor/gitignore/Typo3.gitignore
new file mode 100644
index 00000000000..cb024fefe99
--- /dev/null
+++ b/vendor/gitignore/Typo3.gitignore
@@ -0,0 +1,20 @@
+## TYPO3 v6.2
+# Ignore several upload and file directories.
+/fileadmin/user_upload/
+/fileadmin/_temp_/
+/fileadmin/_processed_/
+/uploads/
+# Ignore cache
+/typo3conf/temp_CACHED*
+/typo3conf/temp_fieldInfo.php
+/typo3conf/deprecation_*.log
+/typo3conf/AdditionalConfiguration.php
+# Ignore system folders, you should have them symlinked.
+# If not comment out the following entries.
+/typo3
+/typo3_src
+/typo3_src-*
+/.htaccess
+/index.php
+# Ignore temp directory.
+/typo3temp/
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
new file mode 100644
index 00000000000..ea05e1fb2a9
--- /dev/null
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -0,0 +1,19 @@
+# Note: VisualStudio gitignore rules may also be relevant
+
+# Umbraco
+# Ignore unimportant folders generated by Umbraco
+**/App_Data/Logs/
+**/App_Data/[Pp]review/
+**/App_Data/TEMP/
+**/App_Data/NuGetBackup/
+
+# Ignore Umbraco content cache file
+**/App_Data/umbraco.config
+
+# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder)
+# Make sure to include details from VisualStudio.gitignore BEFORE this
+!**/App_Data/[Pp]ackages/
+!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages
+
+# ImageProcessor DiskCache
+**/App_Data/cache/
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
new file mode 100644
index 00000000000..5aafcbb7f1d
--- /dev/null
+++ b/vendor/gitignore/Unity.gitignore
@@ -0,0 +1,30 @@
+/[Ll]ibrary/
+/[Tt]emp/
+/[Oo]bj/
+/[Bb]uild/
+/[Bb]uilds/
+/Assets/AssetStoreTools*
+
+# Autogenerated VS/MD solution and project files
+ExportedObj/
+*.csproj
+*.unityproj
+*.sln
+*.suo
+*.tmp
+*.user
+*.userprefs
+*.pidb
+*.booproj
+*.svd
+
+
+# Unity3D generated meta files
+*.pidb.meta
+
+# Unity3D Generated File On Crash Reports
+sysinfo.txt
+
+# Builds
+*.apk
+*.unitypackage
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
new file mode 100644
index 00000000000..be0e4913c3a
--- /dev/null
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -0,0 +1,63 @@
+# Visual Studio 2015 user specific files
+.vs/
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+*.ipa
+
+# These project files can be generated by the engine
+*.xcodeproj
+*.sln
+*.suo
+*.opensdf
+*.sdf
+*.VC.db
+*.VC.opendb
+
+# Precompiled Assets
+SourceArt/**/*.png
+SourceArt/**/*.tga
+
+# Binary Files
+Binaries/*
+
+# Builds
+Build/*
+
+# Don't ignore icon files in Build
+!Build/**/*.ico
+
+# Configuration files generated by the Editor
+Saved/*
+
+# Compiled source files for the engine to use
+Intermediate/*
+
+# Cache files for the editor to use
+DerivedDataCache/*
diff --git a/vendor/gitignore/VVVV.gitignore b/vendor/gitignore/VVVV.gitignore
new file mode 100644
index 00000000000..5df4324603e
--- /dev/null
+++ b/vendor/gitignore/VVVV.gitignore
@@ -0,0 +1,6 @@
+
+# .v4p backup files
+*~.xml
+
+# Dynamic plugins .dll
+bin/
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
new file mode 100644
index 00000000000..67acbf42f5e
--- /dev/null
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -0,0 +1,253 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore
new file mode 100644
index 00000000000..48e8d8f7be4
--- /dev/null
+++ b/vendor/gitignore/Waf.gitignore
@@ -0,0 +1,4 @@
+# for projects that use Waf for building: http://code.google.com/p/waf/
+.waf-*
+.waf3-*
+.lock-*
diff --git a/vendor/gitignore/WordPress.gitignore b/vendor/gitignore/WordPress.gitignore
new file mode 100644
index 00000000000..97923503c4c
--- /dev/null
+++ b/vendor/gitignore/WordPress.gitignore
@@ -0,0 +1,18 @@
+*.log
+wp-config.php
+wp-content/advanced-cache.php
+wp-content/backup-db/
+wp-content/backups/
+wp-content/blogs.dir/
+wp-content/cache/
+wp-content/upgrade/
+wp-content/uploads/
+wp-content/wp-cache-config.php
+wp-content/plugins/hello.php
+
+/.htaccess
+/license.txt
+/readme.html
+/sitemap.xml
+/sitemap.xml.gz
+
diff --git a/vendor/gitignore/Xojo.gitignore b/vendor/gitignore/Xojo.gitignore
new file mode 100644
index 00000000000..1b036dd4f2e
--- /dev/null
+++ b/vendor/gitignore/Xojo.gitignore
@@ -0,0 +1,11 @@
+# Xojo (formerly REALbasic and Real Studio)
+
+Builds*
+*.debug
+*.debug.app
+Debug*.exe
+Debug*/Debug*.exe
+Debug*/Debug*\ Libs
+*.rbuistate
+*.xojo_uistate
+*.obsolete
diff --git a/vendor/gitignore/Yeoman.gitignore b/vendor/gitignore/Yeoman.gitignore
new file mode 100644
index 00000000000..7170d72018d
--- /dev/null
+++ b/vendor/gitignore/Yeoman.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+bower_components/
+*.log
+
+build/
+dist/
diff --git a/vendor/gitignore/Yii.gitignore b/vendor/gitignore/Yii.gitignore
new file mode 100644
index 00000000000..70f087546f2
--- /dev/null
+++ b/vendor/gitignore/Yii.gitignore
@@ -0,0 +1,6 @@
+assets/*
+!assets/.gitignore
+protected/runtime/*
+!protected/runtime/.gitignore
+protected/data/*.db
+themes/classic/views/
diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore
new file mode 100644
index 00000000000..80adb154900
--- /dev/null
+++ b/vendor/gitignore/ZendFramework.gitignore
@@ -0,0 +1,25 @@
+# Composer files
+composer.phar
+vendor/
+
+# Local configs
+config/autoload/*.local.php
+
+# Binary gettext files
+*.mo
+
+# Data
+data/logs/
+data/cache/
+data/sessions/
+data/tmp/
+temp/
+
+#Doctrine 2
+data/DoctrineORMModule/Proxy/
+data/DoctrineORMModule/cache/
+
+
+# Legacy ZF1
+demos/
+extras/documentation
diff --git a/vendor/gitignore/Zephir.gitignore b/vendor/gitignore/Zephir.gitignore
new file mode 100644
index 00000000000..839cb5d7070
--- /dev/null
+++ b/vendor/gitignore/Zephir.gitignore
@@ -0,0 +1,26 @@
+# Cache files, generates by Zephir
+.temp/
+.libs/
+
+# Object files, generates by linker
+*.lo
+*.la
+*.o
+*.loT
+
+# Files generated by configure and Zephir,
+# not required for extension compilation.
+ext/build/
+ext/modules/
+ext/Makefile*
+ext/config*
+ext/acinclude.m4
+ext/aclocal.m4
+ext/autom4te*
+ext/install-sh
+ext/ltmain.sh
+ext/missing
+ext/mkinstalldirs
+ext/run-tests.php
+ext/.deps
+ext/libtool
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
new file mode 100644
index 00000000000..396d3f1b042
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -0,0 +1,7 @@
+# Official docker image.
+image: docker:latest
+
+build:
+ stage: build
+ script:
+ - docker build -t test .
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
new file mode 100644
index 00000000000..0b329aaf1c4
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -0,0 +1,18 @@
+# This template uses the non default language docker image
+# The image already has Hex installed. You might want to consider to use `elixir:latest`
+image: trenpixster/elixir:latest
+
+# Pic zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+before_script:
+ - mix deps.get
+
+mix:
+ script:
+ - mix test
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
new file mode 100644
index 00000000000..80f7b87b6c0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 GitLab.org
+
+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.
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
new file mode 100644
index 00000000000..1678a47f9ac
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -0,0 +1,102 @@
+---
+# Build JAVA applications using Apache Maven (http://maven.apache.org)
+# For docker image tags see https://hub.docker.com/_/maven/
+#
+# For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
+#
+# This template will build and test your projects as well as create the documentation.
+#
+# * Caches downloaded dependencies and plugins between invocation.
+# * Does only verify merge requests but deploy built artifacts of the
+# master branch.
+# * Shows how to use multiple jobs in test stage for verifying functionality
+# with multiple JDKs.
+# * Uses site:stage to collect the documentation for multi-module projects.
+# * Publishes the documentation for `master` branch.
+
+variables:
+ # This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
+ # `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
+ MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
+ # As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
+ # when running from the command line.
+ # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
+ MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
+
+# Cache downloaded dependencies and plugins between builds.
+cache:
+ paths:
+ - /root/.m2/repository/
+
+# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
+# Because some enforcer rules might check dependency convergence and class duplications
+# we use `test-compile` here instead of `validate`, so the correct classpath is picked up.
+.validate: &validate
+ stage: build
+ script:
+ - 'mvn $MAVEN_CLI_OPTS test-compile'
+
+# For merge requests do not `deploy` but only run `verify`.
+# See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
+.verify: &verify
+ stage: test
+ script:
+ - 'mvn $MAVEN_CLI_OPTS verify site site:stage'
+ except:
+ - master
+
+# Validate merge requests using JDK7
+validate:jdk7:
+ <<: *validate
+ image: maven:3.3.9-jdk-7
+
+# Validate merge requests using JDK8
+validate:jdk8:
+ <<: *validate
+ image: maven:3.3.9-jdk-8
+
+# Verify merge requests using JDK7
+verify:jdk7:
+ <<: *verify
+ image: maven:3.3.9-jdk-7
+
+# Verify merge requests using JDK8
+verify:jdk8:
+ <<: *verify
+ image: maven:3.3.9-jdk-8
+
+
+# For `master` branch run `mvn deploy` automatically.
+# Here you need to decide whether you want to use JDK7 or 8.
+# To get this working you need to define a volume while configuring your gitlab-ci-multi-runner.
+# Mount your `settings.xml` as `/root/.m2/settings.xml` which holds your secrets.
+# See https://maven.apache.org/settings.html
+deploy:jdk8:
+ # Use stage test here, so the pages job may later pickup the created site.
+ stage: test
+ script:
+ - 'mvn $MAVEN_CLI_OPTS deploy site site:stage'
+ only:
+ - master
+ # Archive up the built documentation site.
+ artifacts:
+ paths:
+ - target/staging
+ image: maven:3.3.9-jdk-8
+
+
+pages:
+ image: busybox:latest
+ stage: deploy
+ script:
+ # Because Maven appends the artifactId automatically to the staging path if you did define a parent pom,
+ # you might need to use `mv target/staging/YOUR_ARTIFACT_ID public` instead.
+ - mv target/staging public
+ dependencies:
+ - deploy:jdk8
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
diff --git a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
new file mode 100644
index 00000000000..e5bce3503f3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/node/tags/
+image: node:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - node_modules/
+
+test_async:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/async.spec.js
+
+test_db:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/db-postgres.spec.js
diff --git a/vendor/gitlab-ci-yml/Pages/Brunch.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Brunch.gitlab-ci.yml
new file mode 100644
index 00000000000..7fcc0b436b5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Brunch.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/brunch
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g brunch
+ - brunch build --production
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Doxygen.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Doxygen.gitlab-ci.yml
new file mode 100644
index 00000000000..791afdd23f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Doxygen.gitlab-ci.yml
@@ -0,0 +1,13 @@
+# Full project: https://gitlab.com/pages/doxygen
+image: alpine
+
+pages:
+ script:
+ - apk update && apk add doxygen
+ - doxygen doxygen/Doxyfile
+ - mv doxygen/documentation/html/ public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/HTML.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/HTML.gitlab-ci.yml
new file mode 100644
index 00000000000..249a168aa33
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/HTML.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/plain-html
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Harp.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Harp.gitlab-ci.yml
new file mode 100644
index 00000000000..dd3ef149668
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Harp.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/harp
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules
+
+ script:
+ - npm install -g harp
+ - harp compile ./ public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
new file mode 100644
index 00000000000..b468d79bcad
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hexo
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
new file mode 100644
index 00000000000..45df6975259
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
@@ -0,0 +1,11 @@
+# Full project: https://gitlab.com/pages/hugo
+image: publysher/hugo
+
+pages:
+ script:
+ - hugo
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Hyde.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hyde.gitlab-ci.yml
new file mode 100644
index 00000000000..f5b40f2b9f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Hyde.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
new file mode 100644
index 00000000000..36918fc005a
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
@@ -0,0 +1,24 @@
+# Full project: https://gitlab.com/pages/jekyll
+image: ruby:2.3
+
+test:
+ stage: test
+ script:
+ - gem install jekyll
+ - jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Lektor.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Lektor.gitlab-ci.yml
new file mode 100644
index 00000000000..c5c44a5d86c
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Lektor.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+pages:
+ script:
+ - pip install lektor
+ - lektor build --output-path public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Metalsmith.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Metalsmith.gitlab-ci.yml
new file mode 100644
index 00000000000..50e8b7ccd46
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Metalsmith.gitlab-ci.yml
@@ -0,0 +1,17 @@
+# Full project: https://gitlab.com/pages/metalsmith
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g metalsmith
+ - npm install
+ - make build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml
new file mode 100644
index 00000000000..9f4cc0574d6
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Full project: https://gitlab.com/pages/middleman
+image: ruby:2.3
+
+cache:
+ paths:
+ - vendor
+
+test:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ except:
+ - master
+
+pages:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Nanoc.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Nanoc.gitlab-ci.yml
new file mode 100644
index 00000000000..b469b316ba5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Nanoc.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/nanoc
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install -j4
+ - nanoc
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Octopress.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Octopress.gitlab-ci.yml
new file mode 100644
index 00000000000..4762ec9acfd
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Octopress.gitlab-ci.yml
@@ -0,0 +1,15 @@
+# Full project: https://gitlab.com/pages/octopress
+image: ruby:2.3
+
+pages:
+ script:
+ - apt-get update -qq && apt-get install -qq nodejs
+ - bundle install -j4
+ - bundle exec rake generate
+ - mv public .public
+ - mv .public/octopress public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Pelican.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Pelican.gitlab-ci.yml
new file mode 100644
index 00000000000..c5f3154f587
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Pelican.gitlab-ci.yml
@@ -0,0 +1,10 @@
+# Full project: https://gitlab.com/pages/pelican
+image: python:2.7-alpine
+
+pages:
+ script:
+ - pip install -r requirements.txt
+ - pelican -s publishconf.py
+ artifacts:
+ paths:
+ - public/
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
new file mode 100644
index 00000000000..2a761bbd127
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -0,0 +1,37 @@
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/ruby/tags/
+image: "ruby:2.3"
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# Cache gems in between builds
+cache:
+ paths:
+ - vendor/ruby
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - ruby -v # Print out ruby version for debugging
+ - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image
+ - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
+
+# Optional - Delete if not using `rubocop`
+rubocop:
+ script:
+ - rubocop
+
+rspec:
+ script:
+ - rspec spec
+
+rails:
+ script:
+ - bundle exec rake db:migrate
+ - bundle exec rake test
diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
new file mode 100644
index 00000000000..ae3f7405ea3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
@@ -0,0 +1,23 @@
+# Unofficial language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/scorpil/rust/tags/
+image: "scorpil/rust:stable"
+
+# Optional: Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+#services:
+# - mysql:latest
+# - redis:latest
+# - postgres:latest
+
+# Optional: Install a C compiler, cmake and git into the container.
+# You will often need this when you (or any of your dependencies) depends on C code.
+#before_script:
+#- apt-get update -yqq
+#- apt-get install -yqq --no-install-recommends build-essential
+
+# Use cargo to test the project
+test:cargo:
+ script:
+ - rustc --version && cargo --version # Print version info for debugging
+ - cargo test --verbose --jobs 1 --release # Don't paralize to make errors more readable
diff --git a/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml b/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml
new file mode 100644
index 00000000000..443ba42e38c
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Scala.gitlab-ci.yml
@@ -0,0 +1,22 @@
+# Official Java image. Look for the different tagged releases at
+# https://hub.docker.com/r/library/java/tags/ . A Java image is not required
+# but an image with a JVM speeds up the build a bit.
+image: java:8
+
+before_script:
+ # Enable the usage of sources over https
+ - apt-get update -yqq
+ - apt-get install apt-transport-https -yqq
+ # Add keyserver for SBT
+ - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ # Install SBT
+ - apt-get update -yqq
+ - apt-get install sbt -yqq
+ # Log the sbt version
+ - sbt sbt-version
+
+test:
+ script:
+ # Execute your project's tests
+ - sbt clean test